Zh cn:SQL (SourceMod Scripting)

From AlliedModders Wiki
Revision as of 02:46, 28 June 2019 by MisakaSora (talk | contribs)
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 
{
	/* Process results here!
	 */
 
	/* Free the Handle */
	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];
 
	/* Create enough space to make sure our string is quoted properly  */
	int buffer_len = strlen(name) * 2 + 1;
	char[] new_name = new char[buffer_len];
 
	/* Ask the SQL driver to make sure our string is safely quoted */
	SQL_EscapeString(db, name, new_name, buffer_len);
 
	/* Build the query */
	Format(query, sizeof(query), "SELECT userid FROM vb_user WHERE username = '%s'", new_name);
 
	/* Execute the query */
	if ((hQuery = SQL_Query(query)) == null)
	{
		return 0;
	}
 
	/* Get some info here
	 */
 
	delete hQuery;
}

Observe a version with prepared statements:

DBStatement hUserStmt = null;
int GetSomeInfo(Database db, const char[] name)
{
	/* Check if we haven't already created the statement */
	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;
	}
 
	/**
	 * Get some info here
	 */
}

The important differences:

  • The input string (name) did not need to be backticked (quoted). The SQL engine automatically performs all type safety and insertion checks.
  • There was no need for quotation marks around the parameter marker, ?, even though it accepted a string.
  • We only needed to create the statement Handle once; after that it can live for the lifetime of the database connection.

处理查询结果

Processing results is done in the same manner for both normal queries and prepared statements. The important functions are:

  • SQL_GetRowCount() - Returns the number of rows.
  • SQL_FetchRow() - Fetches the next row if one is available.
  • SQL_Fetch[Int|String|Float]() - Fetches data from a field in the current row.

Let's consider a sample table that looks like this:

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

The following example has code that will print out all users matching a certain age. There is an example for both prepared statements and normal queries.

void PrintResults(Handle query)
{
	/* Even if we have just one row, you must call SQL_FetchRow() first */
	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;
}

Note that these examples did not close the statement Handles. These examples assume a global database instance that is only closed when the plugin is unloaded. For plugins which maintain temporary database connections, prepared statement Handles must be freed or else the database connection will never be closed.

线程

SourceMod supports threaded SQL querying. That is, SQL operations can be completed in a separate thread from main gameplay. If your database server is remote or requires a network connection, queries can cause noticeable gameplay lag, and supporting threading is often a good idea if your queries occur in the middle of gameplay.

Threaded queries are asynchronous. That is, they are dispatched and you can only find the results through a callback. Although the callback is guaranteed to fire eventually, it may not fire in any specific given timeframe. Certain drivers may not support threading; if this is the case, an RTE will be thrown. If the threader cannot start or the threader is currently disabled, SourceMod will transparently execute the query in the main thread as a fallback.

操作

All threaded operations (except connecting) use the same callback for result notification: SQLQueryCallback. The parameters are:

  • db - The cloned database handle. If the db handle was not found or was invalidated, null is passed.
  • results - Result object, or null on failure.
  • error - An error string.
  • data - Custom data that can be passed via some SQL operations.

The following operations can be done via threads:

  • Connection, via SQL_TConnect.
  • Querying, via SQL_TQuery().
  • Note: prepared statements are not yet available for the threader.

It is always safe to chain one operation from another.

连接

It is not necessary to make a threaded connection in order to make threaded queries. However, creating a threaded connection will not pause the game server if a connection cannot be immediately established. Connecting however uses a different callback: SQLConnectCallback.

The following parameters are used for the threaded connection callback:

  • db: Handle to the database connection, or null if it could not be found.
  • error: The error string, if any.
  • data: Unused (0)

Example:

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;
	}
}

查询

Threaded queries can be performed on any database Handle as long as the driver supports threading. All query results for the first result set are retrieved while in the thread. If your query returns more than one set of results (for example, CALL on MySQL with certain functions), the behaviour of the threader is undefined at this time. Note that if you want to perform both threaded and non-threaded queries on the same connection, you MUST read the "Locking" section below.

Query operations use the following callback parameters:

  • owner: A Handle to the database used to query. The Handle is not the same as the Handle originally passed; however, it will test positively with SQL_IsSameConnection. The Handle can be cloned but it cannot be closed (it is closed automatically). It may be null in the case of a serious error (for example, the driver being unloaded).
  • hndl: A Handle to the query. It can be cloned, but not closed (it is closed automatically). It may be null if there was an error.
  • error: Error string, if any.
  • data: Optional user-defined data passed in through SQL_TQuery().

Example, continuing from above:

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;
 
	/* Make sure the client didn't disconnect while the thread was running */
	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 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 is a fast local-file SQL database engine and SourceMod provides a DBI driver for it. SQLite differs from MySQL, and thus MySQL queries may not work in SQLite. The driver type for connections is sqlite.

使用

Since SQLite is local only, most of the connection parameters can be ignored. The only connection parameter required is database, which specifies the file name of the database. Databases are created on demand if they do not already exist, and are stored in addons/sourcemod/data/sqlite. An extension of ".sq3" is automatically appended to the file name.

Additionally, you can specify sub-folders in the database name. For example, "cssdm/players" will become addons/sourcemod/data/sqlite/cssdm/players.sq3.

SQLite supports the threading layer, and requires all of the same rules as the MySQL driver (including locks on shared connections).

外部连接

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)