redtv_sof3 / libasynql
Requires
- sof3/await-generator: ^2.0.0 || ^3.0.0
This package is not auto-updated.
Last update: 2024-09-24 18:27:22 UTC
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资源中读取。
在每个文件中写下您将要使用的所有查询,使用Prepared Statement File format。
调用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
整型
浮点型
布尔型
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代码风格,并使用它来减少混乱。