SQL (SourceMod Scripting)/zh

From AlliedModders Wiki
Jump to: navigation, search
Language: English  • 中文

本文将介绍如何使用SourceMod的SQL特性。请注意,这并不是关于SQL或任何特定SQL实现的介绍或教程。

SourceMod的SQL层的正式称呼是DBI,全写是Database Interface,中文称呼为数据库接口,这个接口是通用SQL方法的一般抽象。要连接到特定数据库(譬如MySQL和sqLite),则必须加载对应的SourceMod DBI驱动。当前SourceMod已内置有MySQL和SQLite的驱动。

SourceMod会自动按需(当然前提是有)检测并加载驱动。所有的数据库相关内置函数可以在scripting/include/dbi.inc中找到,对应的C++ API则在public/IDBDriver.h中。

连接数据库

当前共有两种方式连接数据库。第一种是通过命名配置文件,命名配置是在configs/databases.cfg中列出的预设配置。如果使用了数据库,那么SourceMod要求你至少提供一个名为"default"的配置。

下面是配置SQL的一个例子:

	"default"
	{
		"host"				"localhost"
		"database"			"sourcemod"
		"user"				"root"
		"pass"				""
		//"timeout"			"0"
		//"port"			"0"
	}

请使用SQL_ConnectSQL_DefConnect来实例化基于命名配置的数据库连接。

另一种方式是使用SQL_ConnectCustom,并通过传递包含这些参数的键值对句柄对象手动指定所有连接参数。

下面是一个典型的连接数据库的例子:

char error[255];
Database db = SQL_DefConnect(error, sizeof(error));
 
if (db == null)
{
	PrintToServer("Could not connect: %s", error);
} 
else 
{
	delete db;
}

查询语句

无返回结果类

最简单的查询就是那些不返回结果的查询了,例如,CREATE,DROP,UPDATE,INSERT和DELETE。对于这些查询语句,推荐使用SQL_FastQuery()。它的名字里带快,并不意味着它执行起来更快,实际上,它只是让我们在编写代码上更快。下面是使用例子,代码中给定的db是一个有效的数据库句柄:

if (!SQL_FastQuery(db, "UPDATE stats SET players = players + 1"))
{
	char error[255];
	SQL_GetError(db, error, sizeof(error));
	PrintToServer("Failed to query (error: %s)", error);
}

有返回结果类

如果一个查询返回了结果集,并且必须处理它,那么你就必须使用SQL_Query()了。与SQL_FastQuery()不同的是,这个函数会返回一个必须关闭的句柄。

一个会返回结果的查询例子:

DBResultSet query = SQL_Query(db, "SELECT userid FROM vb_user WHERE username = 'BAILOPAN'");
if (query == null)
{
	char error[255];
	SQL_GetError(db, error, sizeof(error));
	PrintToServer("Failed to query (error: %s)", error);
} 
else 
{
	/* 在这里进行结果的处理!
	 */
 
	/* 释放句柄对象 */
	delete query;
}

预编译语句

预编译语句是另一种查询的方法。预编译语句背后的实质是,你构造一个查询的“模板”,在这之后可以任意次地复用。预编译语句有以下的优势:

  • 如果使用预编译语句,那么数据库就可以更好地缓存查询
  • 你不需要每次使用时重新构造查询语句
  • 你不需要每次使用时分配新的查询结构体
  • 输入总是安全的(下面会讲到)

A prepared statement has "markers" for inputs. For example, let's consider a function that takes in a database Handle and a name, and retrieves some info from a table:

int GetSomeInfo(Database db, const char[] name)
{
	DBResultSet hQuery;
	char query[100];
 
	/* 创建足够的空间来保证我们的查询字符串能被正确引用  */
	int buffer_len = strlen(name) * 2 + 1;
	char[] new_name = new char[buffer_len];
 
	/* 要求SQL驱动程序确保我们的字符串被安全引用 */
	SQL_EscapeString(db, name, new_name, buffer_len);
 
	/* 生成查询语句串 */
	Format(query, sizeof(query), "SELECT userid FROM vb_user WHERE username = '%s'", new_name);
 
	/* 执行查询语句 */
	if ((hQuery = SQL_Query(query)) == null)
	{
		return 0;
	}
 
	/* 获取一些信息
	 */
 
	delete hQuery;
}

让我们来看看使用预编译语句的版本:

DBStatement hUserStmt = null;
int GetSomeInfo(Database db, const char[] name)
{
	/* 检查一下我们是不是已经创建好了语句 */
	if (hUserStmt == null)
	{
		char error[255];
		hUserStmt = SQL_PrepareQuery(db, "SELECT userid FROM vb_user WHERE username = ?", error, sizeof(error));
		if (hUserStmt == null)
		{
			return 0;
		}
	}
 
	SQL_BindParamString(hUserStmt, 0, name, false);
	if (!SQL_Execute(hUserStmt))
	{
		return 0;
	}
 
	/**
	 * 在这处理信息
	 */
}

重要区别:

  • 输入字符串(name)不需要被反引号(引号)包起来。数据库引擎会自动处理类型安全和插入检查。
  • 参数标记?周围不需要引号,即使它接受了一个字符串。
  • 我们仅仅只需要创建语句句柄一次,在那之后,于该数据库连接的整个生命周期它都将存在。

处理查询结果

对于普通查询和预编译语句查询,它们处理结果的方式相同。 相关的重要函数如下:

  • SQL_GetRowCount() - 返回查询结果的行数。
  • SQL_FetchRow() - 拉取下一行,如果有的话。
  • SQL_Fetch[Int|String|Float]() - 从当前行拉取特定字段的数据。

我们假设有如下结构的一个简单表:(译注:这是一段建表语句)

CREATE TABLE users (
	name VARCHAR(64) NOT NULL PRIMARY KEY,
	age INT UNSIGNED NOT NULL
	);

下面的示例包含的代码将打印出与某个年龄段匹配的所有用户。常规查询和预编译语句各有一个例子。

void PrintResults(Handle query)
{
	/* 即便结果只有一行,你也应该先使用SQL_FetchRow() */
	char name[MAX_NAME_LENGTH];
	while (SQL_FetchRow(query))
	{
		SQL_FetchString(query, 0, name, sizeof(name));
		PrintToServer("Name \"%s\" was found.", name);
	}
}
 
bool GetByAge_Query(Database db, int age)
{
	char query[100];
	Format(query, sizeof(query), "SELECT name FROM users WHERE age = %d", age);
 
	DBResultSet hQuery = SQL_Query(db, query);
	if (hQuery == null)
	{
		return false;
	}
 
	PrintResults(hQuery);
 
	delete hQuery;
 
	return true;
}
 
DBStatement hAgeStmt = null;
bool GetByAge_Statement(Database db, int age)
{
	if (hAgeSmt == null)
	{
		char error[255];
		if ((hAgeStmt = SQL_PrepareQuery(db, 
			"SELECT name FROM users WHERE age = ?", 
			error, 
			sizeof(error))) 
		     == null)
		{
			return false;
		}
	}
 
	SQL_BindParamInt(hAgeStmt, 0, age);
	if (!SQL_Execute(hAgeStmt))
	{
		return false;
	}
 
	PrintResults(hAgeStmt);
 
	return true;
}

注意,这些示例中都没有关闭预编译语句句柄。因为这些示例假定有一个全局的数据库实例,只有在卸载插件时才关闭它。对于那些自己维护临时数据库连接的插件,就必须释放预编译语句句柄,否则数据库连接将永远不会关闭。

多线程

译注:下面的内容比较复杂,请选择性观看。

SourceMod支持多线程SQL查询。这意味着,数据库操作可以在游戏主线程之外的线程中完成。如果你使用远程的数据库服务器或者需要一个网络连接,查询可能会导致明显延迟,所以如果你的查询发生在游戏进行期间,那么支持线程通常是一个好主意。

多线程查询是异步的。这意味着,他们会在回调函数里触发并返回结果。尽管回调函数最终一定能触发,但是你并不能让它在指定时刻触发。某些驱动可能不支持多线程,如果是这种情况,一个运行时错误会被抛出。如果线程器不能被启动或者被禁用,SourceMod会以回调函数的形式在主线程里执行查询。

操作

所有被线程化的操作(除了数据库连接)都使用一样的回调SQLQueryCallback来接受结果,参数如下:

  • db - 数据库句柄对象的拷贝。如果db句柄找不到或者是无效的,会把 null 传过去。
  • results - 结果对象,失败时为null。
  • error - 包含错误内容的字符串。
  • data - 通过SQL操作传递的自定义数据。

多线程支持下面的操作:

  • 数据库连接,使用函数 SQL_TConnect
  • 数据库查询,使用函数 SQL_TQuery()
  • 注意: 预编译语句目前不支持多线程。

数据库操作的连续调用是安全的。

连接

没必要为了线程化的查询而使用线程化的数据据连接。但是,使用线程化的数据库连接就不会因为建立连接时的延时造成服务器卡顿。 线程化的数据库连接使用这个回调: SQLConnectCallback

回调的参数如下:

  • db: 数据库连接的句柄对象,如果无法连接,将会返回null
  • error: 错误的字符串,如果有
  • data: 未使用

例子:

Database hDatabase = null;
 
void StartSQL()
{
	Database.Connect(GotDatabase);
}
 
public void GotDatabase(Database db, const char[] error, any data)
{
	if (db == null)
	{
		LogError("Database failure: %s", error);
	} 
        else 
        {
		hDatabase = db;
	}
}

查询

只要驱动支持,那么线程化的查询可以用任何的数据库句柄来执行。在线程中,第一个结果集的所有查询结果都会被检索。如果你的查询会返回不止一个结果集(比如,在MySQL中用CALL调用特定函数),那么这个时候线程器的行为是未定义的。请注意,如果你想在同一个连接上同时执行线程和非线程化的查询,你应当先阅读下面有关“线程锁”的小节

查询操作使用如下的回调参数:

  • owner: 一个指向进行查询的数据库的对象。这个对象与原本传入的对象不同,但是,使用SQL_IsSameConnection方法对二者进行比较将会返回真值。这个对象可以被复制,但是不能被手动关闭(它将会自动关闭)。在出现严重错误时(例如,驱动被卸载),它将会是null。
  • hndl: 一个指向查询的对象,它可以被复制,但是不能被手动关闭(将会被自动关闭)。如果有错误,这个对象将会是null
  • error: 错误的字符串,如果有、
  • data: 通过SQL_TQuery()<tt>传入的、可选的、用户自定义数据。

一个接着上面代码的例子:

void CheckSteamID(int userid, const char[] auth)
{
	char query[255];
	FormatEx(query, sizeof(query), "SELECT userid FROM users WHERE steamid = '%s'", auth);
	hdatabase.Query(T_CheckSteamID, query, userid);
}
 
public void T_CheckSteamID(Database db, DBResultSet results, const char[] error, any data)
{
	int client = 0;
 
	/* 确保玩家不会在线程运行的时候断开连接 */
	if ((client = GetClientOfUserId(data)) == 0)
	{
		return;
	}
 
	if (results == null)
	{
		LogError("Query failed! %s", error);
		KickClient(client, "Authorization failed");
	} else if (results.RowCount == 0) {
		KickClient(client, "You are not a member");
	}
}

It is possible to run both threaded and non-threaded queries on the same connection. However, without the proper precautions, you could corrupt the network stream (even if it's local), corrupt memory, or otherwise cause a crash in the SQL driver. To solve this, SourceMod has database locking. Locking is done via <tt>SQL_LockDatabase() and SQL_UnlockDatabase.

Whenever performing any of the following non-threaded operations on a database, it is absolutely necessary to enclose the entire operation with a lock:

  • SQL_Query() (and SQL_FetchMoreResults pairings)
  • SQL_FastQuery
  • SQL_PrepareQuery
  • SQL_Bind* and SQL_Execute pairings

The rule of thumb is: if your operation is going to use the database connection, it must be locked until the operation is fully completed.

Example:

bool GetByAge_Query(Database db, int age)
{
	char query[100];
	FormatEx(query, sizeof(query), "SELECT name FROM users WHERE age = %d", age);
 
	SQL_LockDatabase(db);
	DBResultSet hQuery = SQL_Query(db, query);
	if (hQuery == null)
	{
		SQL_UnlockDatabase(db);
		return false;
	}
	SQL_UnlockDatabase(db);
 
	PrintResults(hQuery);
 
	delete hQuery;
 
	return true;
}

Note that it was only necessary to lock the query; SourceMod pre-fetches the result set, and thus the network queue is clean.

警告

  • Never call SQL_LockDatabase right before a threaded operation. You will deadlock the server and have to terminate/kill it.
  • Always pair every Lock with an Unlock. Otherwise you risk a deadlock.
  • If your query returns multiple result sets, for example, a procedure call on MySQL that returns results, you must lock both the query and the entire fetch operation. SourceMod is only able to fetch one result set at a time, and all result sets must be cleared before a new query is started.

优先级

Threaded SQL operations are placed in a simple priority queue. The priority levels are High, Medium, and Low. Connections always have the highest priority.

Changing the priority can be useful if you have many queries with different purposes. For example, a statistics plugin might execute 10 queries on death, and one query on join. Because the statistics might rely on the join info, the join query might need to be high priority, while the death queries can be low priority.

You should never simply assign a high priority to all of your queries simply because you want them to get done fast. Not only does it not work that way, but you may be inserting subtle problems into other plugins by being greedy.


SQLite

简介

SQLite是一个基于本地文件的数据库引擎。SourceMod内置了对应DBI。SQLite和MySql是不同的数据库,所以有些Mysql的查询语句在SQLite中将不会生效。在配置文件中它对应的数据库类型填sqlite

使用

大部分的连接参数是可以忽略的,因为SQLite是本地数据库。唯一需要的连接参数是database,这个指定了对应数据库的文件名称。如果数据库不存在,对应数据库会按需创建,相应文件存储在addons/sourcemod/data/sqlite目录下,并自动设置文件扩展名为“.sql3”。

另外,你可以通过数据库名称来指定子文件夹。例如,“cssdm/player”对应的文件会是addons/sourcemod/data/sqlite/cssdm/players.sql3

SQLite支持线程层,并且和MySQL遵循同样的规则。(包括锁和共享连接)

外部链接

Warning: This template (and by extension, language format) should not be used, any pages using it should be switched to Template:Languages

View this page in:  English  Russian  简体中文(Simplified Chinese)