📦 數據庫 API
數據庫,一般用於插件可持久化地儲存某些插件所生成和處理的數據。
不同於配置文件,數據庫一般沒有可讀性方面的要求,而對性能和穩定性有相當的考慮。
腳本引擎提供統一數據庫接口來完成這個任務。
具體實現上,引擎提供了兩種不同的數據庫格式:鍵 - 值對格式的NoSQL數據庫,和表格形式的SQL數據庫。你可以按需使用。
📄 目錄
🔑 KVDB 鍵值對數據庫
📋 SQL數據庫
- 🔍 預準備語句
🔑 鍵 - 值對 NoSQL數據庫
鍵 - 值對數據庫適用於儲存鍵 - 值形式的數據, 形如name:apple, value:5等等。
底層使用 LevelDB 實現
創建 / 打開一個鍵值對數據庫
在使用數據庫之前,你先需要給出一個數據庫路徑,接口將打開/創建指定的數據庫並返回一個數據庫對象。
一個LevelDB數據庫是由多個文件組成的,所以你需要傳入一個文件夾的路徑,數據庫文件會被儲存於這個文件夾當中。
如果這個目錄已含有一個數據庫,將打開它,否則會新建一個。
[JavaScript] new KVDatabase(dir)
[Lua] KVDatabase(dir)
- 參數:
- dir :
String
數據庫的儲存目錄路徑,以BDS根目錄為基準 - 返回值:打開 / 創建的數據庫對象
- 返回值類型:
KVDatabase - 如果返回值為
Null,則代表創建 / 打開失敗
當給出的目錄不存在時,將嘗試自動逐層創建對應的目錄路徑
成功打開數據庫後,你可以使用下面的接口來進行相關的操作。
對於一個數據庫對象db,有以下這些函數:
寫入數據項
db.set(name,data)
- 參數:
- name :
String
數據項名字 - data :
任意类型
要寫入的數據。允許使用的數據類型有:
IntegerFloatStringBooleanArrayObject
其中,Array和Object內部僅能嵌套上面出現的這些元素 - 返回值:是否寫入成功
- 返回值類型:
Boolean
讀取數據項
db.get(name)
- 參數:
- name :
String
數據項名字 - 返回值:數據庫中儲存的這個項的數據
- 返回值類型:
任意类型,以具體儲存的數據類型為準 - 如返回值為
Null則表示數據不存在
刪除數據項
db.delete(name)
- 參數:
- name :
String
數據項名字 - 返回值:是否成功刪除
- 返回值類型:
Boolean
獲取所有數據項名字
db.listKey()
- 返回值:所有的數據項名字數組
- 返回值類型:
Array
關閉數據庫
db.close()
- 返回值:是否成功關閉
數據庫關閉之後,請勿繼續使用!
📋 SQL數據庫
SQL數據庫適用於使用SQL語句處理大量的關係型數據。接口底層使用跨數據庫操作框架實現,可對接絕大多數市面常用SQL數據庫。
注:以下API若未註明均可能會拋出異常,建議使用各語言的異常處理語句嵌套。JavaScript可使用try ... catch語句,Lua可使用pcall。一般情況下未拋出錯誤即代表調用成功。
如果您是JavaScript插件開發者,您還可以嘗試使用Yoyo封裝的LLDB鏈式操作庫(主要面向不瞭解SQL語法的新手開發者)。詳情 點擊這裡
打開一個SQL數據庫會話
由於需要做到多數據庫兼容, 連接數據庫需要傳入一個包含連接參數的對象或字符串。
[JavaScript] new DBSession(type, params)
[Lua] DBSession(type, params)
- 參數:
- type :
String
數據庫的類型,支持"sqlite3"和"mysql" - params:
Object
連接參數 - 返回值:打開的數據庫會話對象
- 返回值類型:
DBSession - 如果返回值為
Null,則代表打開失敗
[JavaScript] new DBSession(str)
[Lua] DBSession(str)
- 參數:
- str :
String
形如file:///mydb.db?k=v,mysql://root:password@localhost:3306/db的連接字符串 - 返回值:打開的數據庫會話對象
- 返回值類型:
DBSession - 如果返回值為
Null,則代表打開失敗
連接參數
| 鍵 | 用途 | 可用數據庫 | 示例 | 默認值 |
|---|---|---|---|---|
| path | 指定數據庫所在路徑 | SQLite | plugins/test.db | - |
| create | 數據庫不存在是否自動創建 | SQLite | true/false | true |
| readonly | 以只讀模式打開 | SQLite | true/false | false |
| readwrite | 以讀寫模式打開 | SQLite | true/false | true |
執行SQL並獲取結果集
session.query(sql)
- 參數:
- sql :
String
要查詢的SQL語句 - 返回值:查詢的結果(結果集)
- 返回值類型:
Array<Array>
返回數組的第1行(result[0])為結果集的表頭(列名),剩餘行為結果數據
若查詢結果為: | a | b | | --- | --- | | ll | 233 | | h | 114 |
則用query方法返回值表示為
[
["a", "b"],
["ll", 233],
["h", 114]
]執行SQL但不獲取結果
session.exec(sql)
session.execute(sql)
- 參數:
- sql :
String
要執行的SQL語句 - 返回值:處理完畢的會話對象(便於連鎖進行其他操作)
- 返回值類型:
DBSession
獲取當前會話是否為打開狀態
session.isOpen()
- 返回值:是否為打開狀態
- 返回值類型:
Boolean
備份數據庫
session.backup(path)
- 參數:
- path :
String備份數據庫文件的目標路徑 - 返回值:備份操作是否成功
- 返回值類型:
Boolean
目前該接口只為 SQLite 會話實現。
關閉數據庫會話
session.close()
- 返回值:關閉成功與否
- 返回值類型:
Boolean
SQL預準備語句
預準備語句(Prepared Statement)是SQL的一個重要部分。它的實現原理是:先將含有未知參數的SQL語句(發往服務端)處理、編譯,再綁定參數,最終執行並返回結果。各個SQL的預準備語句實現可能不同,其預準備語句的表示方法也可能存在差異,所以請務必仔細閱讀文檔(直接去閱讀對應SQL的官方文檔則更好)。
預準備語句的主要作用是防止SQL注入攻擊——一種很常見的、危險的攻擊。如果在未經檢驗的情況下直接使用用戶輸入的數據(就像BDS一樣 xD),就可能會造成免密碼登錄甚至數據丟失(注入執行DROP TABLE或DROP DATABASE)等嚴重後果。所以在處理用戶輸入的數據時,更推薦使用預準備語句。其次,它可以在(服務器)只編譯一次語句的情況下,實現多次輸入。
準備一個預準備語句
session.prepare(sql)
- 參數:
- sql :
String
要準備的SQL語句 - 返回值:預準備語句,失敗拋出錯誤
- 返回值類型:
DBStmt
各個SQL的預準備語句
SQLite:
-- 单个?表示参数
SELECT * FROM table WHERE id = ?;
-- ?X和?Y,其中X、Y均为参数名,后面可以更方便地绑定
INSERT INTO table VALUES (?X, ?Y);
-- $X、?Y、:Z、@V都是带参数名的参数,您也可以使用$/:/@ + name的形式定义参数
-- @符号在其他SQL中可能存在特殊含义,建议避免使用@开头
INSERT INTO table VALUES ($X, ?Y, :Z, @V);
/* 注: https://www.sqlite.org/c3ref/bind_blob.html */MySQL:
-- 单个?表示参数
SELECT * FROM table WHERE id = ?;
-- 原生MySQL并不支持含参数名的参数,LLDB为了兼容其他SQL简单实现了参数名解析,建议避免使用
-- ?X和?Y,其中X、Y均为参数名,后面可以更方便地绑定
INSERT INTO table VALUES (?X, ?Y);
-- $X、?Y、:Z都是带参数名的参数,您也可以使用$/:/ + name的形式定义参数
INSERT INTO table VALUES ($X, ?Y, :Z);預準備語句對象 - 屬性
| 屬性 | 含義 | 類型 | 另見 |
|---|---|---|---|
stmt.affectedRows | 獲取該預準備語句執行後影響的行數(僅對INSERT UPDATE DELETE REPLACE 等語句生效) | Integer | SQLite MySQL |
stmt.insertId | 獲取該INSERT/UPDATE/REPLACE語句執行後最後一個更改行的行號(關於行號的解釋詳見官方文檔) | Integer | SQLite MySQL |
這些對象屬性都是隻讀的,無法被修改,並且只能在語句執行之後獲取到
綁定參數到一個SQL語句
stmt.bind(val)
- 參數:
- val :
任意类型
要綁定的值 - 注:本重載將會將值綁定到第一個未綁定的參數上
stmt.bind(obj)
- 參數:
- val :
Object
要綁定的對象,等同於遍歷此對象並執行bind(val, key)
stmt.bind(arr)
- 參數:
- arr :
Array
要綁定的數組,等同於遍歷此數組並執行bind(val)
stmt.bind(val, index)
- 參數:
- val :
任意类型
要綁定的值 - index :
Integer
要綁定到的參數索引(從0開始)
stmt.bind(val, name)
-
參數:
-
val :
任意类型
要綁定的值 -
*name :
String
要綁定到的參數的參數名 -
返回值:處理完畢的語句對象(便於連鎖進行其他操作)
-
返回值類型:
DBStmt
在綁定完成後,應調用stmt.execute()執行語句,否則語句將不會被執行
一個樣例搞懂幾個重載函數
let stmt = session.prepare("INSERT INTO table VALUSE ($a, $b, $c, $d, $e, $f, $g, $h)");
let values = {
c: "have you",
d: "finished",
e: "your",
f: "homework?"
};
stmt.bind(values); // c,d,e,f将会被绑定
stmt.bind("LLSE"); // 将会绑定到a
stmt.bind(["****", "mojang"]); // 将会绑定到b和g
stmt.bind(114514, 7); // 将会绑定到h執行當前語句
stmt.execute()
- 返回值:- 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
- 返回值類型:
DBStmt
步進到下一行結果
stmt.step()
stmt.next()
- 返回值:執行成功與否
- 返回值類型:
Boolean - 注意:所有參數綁定完成後會自動執行語句,執行完後此時的step就在第一行上,所以應使用do...while語句遍歷,而不是while語句,否則將導致第一行被跳過
獲取當前結果行
stmt.fetch()
- 返回值:當前結果行,形如
{col1: "value", col2: 2333} - 返回值類型:
Object
獲取所有結果行
stmt.fetchAll()
- 返回值:查詢的結果(結果集),詳見執行SQL並獲取結果集
- 返回值類型:
Array<Array>
stmt.fetchAll(callback)
- 參數:
- callback :
Function<bool(Object)>
回調函數,用於遍歷結果行;在回調函數中返回false可終止遍歷 - 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
- 返回值類型:
DBStmt
重置當前語句狀態至“待執行”
stmt.reset()
- 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
- 返回值類型:
DBStmt - 注意:本函數不會清除已綁定的參數
重新執行預準備語句
stmt.reexec()
- 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
- 返回值類型:
DBStmt - 注意:本函數是一個便捷函數,等同於執行
stmt.reset()和stmt.execute()
清除所有已綁定的參數
stmt.clear()
- 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
- 返回值類型:
DBStmt
樣例
let dat = {};
let modified = {};
let session = null;
function initdb() {
if (!file.exists("plugins/MyPlugin")) file.mkdir("plugins/MyPlugin");
session = new DBSession("sqlite", {path: "./plugins/MyPlugin/dat.db"});
session.exec(`
CREATE TABLE IF NOT EXISTS "test" (
player CHAR(100) NOT NULL,
coins INTEGER NOT NULL
);`); // 这里创建数据表后SQLite会自动添加一个隐藏的ROWID行,insertId就是这个隐藏的ROWID行的值
// 另:如果数据库自带INTEGER类型主键,则ROWID即为该主键
}
// 使用do...while语句
function loadData() {
let stmt = session.prepare("SELECT * FROM test");
do { // 准备并执行后,默认在第一行
let row = stmt.fetch();
dat[row.player] = row.coins;
} while (stmt.step()); // 第一次执行时步进到第二行,并成功获取到结果,返回true;最后一行时再步进则返回false
}
// 使用回调函数
function loadData2() {
session.prepare("SELECT * FROM test")
.execute()
.fetchAll((player, coins) => {
dat[player] = coins;
});
}
function writeData() {
let keys = Object.keys(modified);
let stmt = session.prepare("UPDATE FROM test WHERE player = ? SET coins = ?").execute();
for (let i = 0; i < keys.length; i++) {
let v = modified[keys[i]];
stmt.bind([keys[i], v]); // 绑定数组参数
// 绑定完成后自动执行语句
stmt.clear(); // 清除已经绑定的值
}
}
mc.regPlayerCmd("getcoin", "Get a coin!", (pl, args)=>{
dat[pl.realName]++;
modified[pl.realName]++;
});