sof3 / libasynql
Requires
- pocketmine/pocketmine-mp: ^5.0.0-BETA4
- sof3/await-generator: ^2.0.0 || ^3.0.0
README
用于 PocketMine 插件的异步 SQL 访问库。
为什么我要使用这个库,异步是什么意思?
在主线程上执行 SQL 查询时,将会有延迟等待 MySQL 服务器或 SQLite 与文件系统交互。这个延迟会阻塞主线程,导致服务器卡顿。
Libasynql 使用 不同的线程执行查询,因此主线程不会卡顿!
如果您想了解更多关于线程的信息,请参阅这里。
用法
libasynql 的基本用法有 5 步
- 在您的
config.yml
中添加默认数据库设置。 - 在资源文件中写下您将使用的所有 SQL 查询。
- 在
onEnable()
中初始化数据库。 - 在
onDisable()
中关闭数据库。 - 显然,最重要的是在您的代码中使用 libasynql。
配置
为了让用户选择要使用哪个数据库,将以下内容复制到您的默认 config.yml
中。请记住更改 mysql
下的默认模式名称。
database: # The database type. "sqlite" and "mysql" are supported. type: sqlite # Edit these settings only if you choose "sqlite". sqlite: # The file name of the database in the plugin data folder. # You can also put an absolute path here. file: data.sqlite # Edit these settings only if you choose "mysql". mysql: host: 127.0.0.1 # Avoid using the "root" user for security reasons. username: root password: "" schema: your_schema # The maximum number of simultaneous SQL queries # Recommended: 1 for sqlite, 2 for MySQL. You may want to further increase this value if your MySQL connection is very slow. worker-limit: 1
初始化和关闭
libasynql 将初始化数据库的过程简化为单个函数调用。
use pocketmine\plugin\PluginBase; use poggit\libasynql\libasynql; class Main extends PluginBase{ private $database; public function onEnable(){ $this->saveDefaultConfig(); $this->database = libasynql::create($this, $this->getConfig()->get("database"), [ "sqlite" => "sqlite.sql", "mysql" => "mysql.sql" ]); } public function onDisable(){ if(isset($this->database)) $this->database->close(); } }
\poggit\libasynql\libasynql::create()
方法接受 3 个参数
- 您的插件主(基本上
$this
如果代码在 onEnable() 中运行) - 配置条目,其中应找到数据库设置(查看上面的示例)
- SQL 文件的数组。对于您支持的每个 SQL 方言,将其用作键,将 SQL 文件的路径(或路径数组,相对于
resources
文件夹)用作值。我们将在 下一步 中创建它们。
它返回一个 \poggit\libasynql\DataConnector
对象,这是主要的查询接口。您可以将此对象存储在属性中以供以后使用,例如 $this->database
。
如果发生错误,将抛出一个 ConfigException 或 SqlError。如果没有被插件捕获,这将直接从 onEnable() 中退出并禁用插件。因此,在 onDisable() 中调用 $this->database->close()
之前,请确保检查 isset($this->database)
。
创建 SQL 文件
在资源文件中,为每个您支持的 SQL 方言创建一个文件,例如 resources/sqlite.sql
和 resources/mysql.sql
。
我需要将 SQL 文件保存到插件数据文件夹中吗?
不需要,您不需要将 SQL 文件复制到插件数据文件夹(即不要添加 $this->saveResource("db.sql")
)。libasynql 会从 phar 资源中直接读取文件。
在每个文件中写下您将要使用的所有查询,使用 预编译语句文件格式。
调用 libasynql 函数
最后,我们准备好在代码中使用 libasynql 了!
您可以使用 4 种查询模式:GENERIC,CHANGE,INSERT 和 SELECT。
- GENERIC:您不想了解查询的任何信息,除了它是否成功。您可能想将它用于
CREATE TABLE
语句。 - 变更:您的查询修改了数据库,您想知道更改了多少行。在
UPDATE
/DELETE
语句中非常有用。 - 插入:您的查询是一个针对具有
AUTO_INCREMENT
键的表的INSERT INTO
查询。您将收到自动递增的行ID。 - 选择:您的查询期望一个结果集,例如一个
SELECT
语句,或者是像EXPLAIN
和SHOW TABLES
这样的反射查询。您将收到一个SqlSelectResult
对象,该对象表示返回的列和行。
它们在DataConnector中都有相应的方法:executeGeneric
、executeChange
、executeInsert
、executeSelect
。它们需要相同的参数
- 预处理语句的名称
- 查询的变量,形式为关联数组 "变量名(不带前面的冒号)" => 值
- 如果查询成功,可选的可调用函数,接受不同的参数
- 通用:无参数
- 变更:
function(int $affectedRows)
- 插入:
function(int $insertId, int $affectedRows)
- 选择:
function(array $rows)
- 如果发生错误,可选的可调用函数。可以接受一个
SqlError
对象。
预处理语句文件格式
预处理语句文件(PSF)包含插件使用的查询。内容是有效的SQL,因此可以使用正常的SQL编辑器进行编辑。
PSF通过“命令行”进行注释,这些命令行以-- #
开头,然后是命令符号,然后是参数。在#
和命令符号之间可以有零到无限个空格或制表符;在命令符号和参数之间也可以有零到无限个空格或制表符。在两个参数之间必须有一个到无限个空格或制表符。
方言声明
PSF始终以方言声明开始。
符号
!
参数
DIALECT
可能的值:mysql
、sqlite
示例
-- #! mysql
组声明
查询可以按组组织。每个组都有一个标识符名称,并且一个组可以嵌套在另一个组下面。组和组下的查询将在它们的标识符前加上父组的标识符加上一个点。
例如,如果父组声明了一个标识符foo
,并且子组/查询声明了一个标识符bar
,则子组/查询的实际标识符是foo.bar
。
允许重复组标识符声明,只要最终的查询没有相同的完整标识符。
符号
- 开始:
{
- 结束:
}
参数(开始)
IDENTIFIER_NAME
本组的名称。
允许除空格和制表符以外的所有字符,包括点。
示例
-- #{ group.name.here -- #{ child.name -- the identifier of the child group is "group.name.here.child.name" -- #} -- #}
请注意,PSF对空格和制表符不敏感,因此这种变体是等效的
-- #{ group.name.here -- # { child.name -- the identifier of the child group is still "group.name.here.child.name" -- # } -- #}
查询声明
查询的声明方式与组类似。查询不需要属于组,因为查询可以在自己的标识符中声明点,这具有与组等效的效果。
查询声明中不允许有子组。换句话说,一个{}
对要么包含其他组/查询声明,要么包含查询文本(以及可选的变量声明),但不能两者都有。
符号
- 开始:
{
(与组声明相同) - 结束:
}
参数
与组声明相同的参数。
变量声明
变量声明声明了此查询所需的变量和可选变量。它只允许在查询声明内。
符号
:
参数
VAR_NAME
变量的名称。允许除空格、制表符和冒号以外的所有字符。然而,为了符合普通的SQL编辑器,建议使用“正常”符号(例如其他编程语言中的变量名)。
VAR_TYPE
变量的类型。可能的值
string
int
float
bool
VAR_DEFAULT
如果变量是可选的,则声明一个默认值。
此参数不受空格影响。它从VAR_TYPE之后的第一个非空格非制表符字符开始,到行的尾部空格或制表符字符之前结束。
string
默认值
有两种模式,字面字符串和JSON字符串。
如果参数以 "
开始并以 "
结束,则整个参数将按JSON解析。否则,整个字符串将被字面地取用。
int
默认值
一个可以被 (int)
强制类型转换的数值,等同于 intval
。
float
默认值
一个可以被 (float)
强制类型转换的数值,等同于 floatval
。
bool
默认值
true
、on
、yes
或 1
将导致真值。其他值,只要不是空值,将导致默认的假值。(如果没有值,则变量不是可选的)
变量使用示例
SQL 文件
-- #! sqlite -- #{ example -- # { insert -- # :foo string -- # :bar int INSERT INTO example( foo_column bar_column ) VALUES ( :foo, :bar ); -- # } -- # { select -- # :foo string -- # :bar int SELECT * FROM example WHERE foo_column = :foo LIMIT :bar; -- # } -- #}
代码
// Example of using variable in insert statements $this->database->executeInsert("example.insert", ["foo" => "sample text", "bar" => 123]); // Example of using variable in select statements $this->database->executeSelect("example.select", ["foo" => "sample text", "bar" => 1], function(array $rows) : void { foreach ($rows as $result) { echo $result["bar_column"]; } });
查询文本
查询文本不是命令,而是查询声明开始和结束命令之间的非注释部分。
使用 :var
格式在查询文本中插入变量。请注意,libasynql 使用一个自制的算法来识别变量位置,因此可能不够准确。
-- #{ query.declarartion SELECT * FROM example; -- The line above is a query text -- #}
注意事项
竞争条件
public $foo = 'bar'; public function setFoo() : void { $this->foo = 'foo'; } public function getFoo() : string { return $this->foo; }
$this->database->executeGeneric("beware.of.race_condition", [], function() : void { $this->setFoo(); }); echo $this->getFoo();
由于查询是异步执行的,结果将是 bar
。主线程上的代码将先于它运行。
为了使代码返回正确的结果,你必须确保在 echo $this->getFoo()
之前运行 $this->setFoo()
。适当的方法是将 getFoo()
移动到回调函数中,就像下面这样
$this->database->executeGeneric("beware.of.race_condition", [], function() : void { $this->setFoo(); echo $this->getFoo(); });
从回调返回结果
由于竞争条件(如上所述),返回结果或在其作用域之外使用结果是不可行的。如果你旨在创建API函数或希望将结果与其他代码部分共享,建议将代码转换为回调。
public function myAPI(\Closure $userCallback) $this->database->executeSelect("beware.of.return_from_callback", [], function($result) use ($userCallback) : void { $userCallback($result); }); // Simpler versions: $this->database->executeSelect("beware.of.return_from_callback", [], fn($result) => $userCallback($result)); $this->database->executeSelect("beware.of.return_from_callback", [], $userCallback); }
回调会弄乱你的代码
虽然使用回调可能是解决你问题的直接解决方案,但存在一个显著的权衡——你必须牺牲代码的可读性。因此,我们建议学习 async/await 代码风格,并使用它来减少混乱。