redtv_sof3/libasynql

dev-main 2024-09-09 19:55 UTC

This package is not auto-updated.

Last update: 2024-09-24 18:27:22 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资源中读取。

在每个文件中写下您将要使用的所有查询,使用Prepared Statement File format

调用libasynql函数

最后,我们准备好在代码中使用libasynql了!

您可以使用4种查询模式:GENERIC,CHANGE,INSERT和SELECT。

  • GENERIC:您不需要知道关于查询的任何信息,除了它是否成功。您可能想将此用于CREATE TABLE语句。
  • 变更:您的查询修改了数据库,您想知道修改了多少行。在 UPDATEDELETE 语句中很有用。
  • 插入:您的查询是一个针对具有 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
  • 整型
  • 浮点型
  • 布尔型
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代码风格,并使用它来减少混乱。

特色示例