sof3/libasynql

4.2.3 2024-04-28 03:17 UTC

README

用于 PocketMine 插件的异步 SQL 访问库。

为什么我要使用这个库,异步是什么意思?

在主线程上执行 SQL 查询时,将会有延迟等待 MySQL 服务器或 SQLite 与文件系统交互。这个延迟会阻塞主线程,导致服务器卡顿。

Libasynql 使用 不同的线程执行查询,因此主线程不会卡顿!

如果您想了解更多关于线程的信息,请参阅这里

用法

libasynql 的基本用法有 5 步

  1. 在您的 config.yml 中添加默认数据库设置。
  2. 在资源文件中写下您将使用的所有 SQL 查询。
  3. onEnable() 中初始化数据库。
  4. onDisable() 中关闭数据库。
  5. 显然,最重要的是在您的代码中使用 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.sqlresources/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语句,或者是像EXPLAINSHOW TABLES这样的反射查询。您将收到一个SqlSelectResult对象,该对象表示返回的列和行。

它们在DataConnector中都有相应的方法:executeGenericexecuteChangeexecuteInsertexecuteSelect。它们需要相同的参数

  • 预处理语句的名称
  • 查询的变量,形式为关联数组 "变量名(不带前面的冒号)" => 值
  • 如果查询成功,可选的可调用函数,接受不同的参数
    • 通用:无参数
    • 变更:function(int $affectedRows)
    • 插入:function(int $insertId, int $affectedRows)
    • 选择:function(array $rows)
  • 如果发生错误,可选的可调用函数。可以接受一个SqlError对象。

预处理语句文件格式

预处理语句文件(PSF)包含插件使用的查询。内容是有效的SQL,因此可以使用正常的SQL编辑器进行编辑。

PSF通过“命令行”进行注释,这些命令行以-- #开头,然后是命令符号,然后是参数。在#和命令符号之间可以有零到无限个空格或制表符;在命令符号和参数之间也可以有零到无限个空格或制表符。在两个参数之间必须有一个到无限个空格或制表符。

方言声明

PSF始终以方言声明开始。

符号

!

参数

DIALECT

可能的值:mysqlsqlite

示例

-- #! 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 默认值

trueonyes1 将导致真值。其他值,只要不是空值,将导致默认的假值。(如果没有值,则变量不是可选的)

变量使用示例

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 代码风格,并使用它来减少混乱。

精选示例