跳到正文
本頁目錄

📦 數據庫 API

數據庫,一般用於插件可持久化地儲存某些插件所生成和處理的數據。
不同於配置文件,數據庫一般沒有可讀性方面的要求,而對性能和穩定性有相當的考慮。
腳本引擎提供統一數據庫接口來完成這個任務。
具體實現上,引擎提供了兩種不同的數據庫格式:鍵 - 值對格式的NoSQL數據庫,和表格形式的SQL數據庫。你可以按需使用。

📄 目錄

🔑 KVDB 鍵值對數據庫
📋 SQL數據庫 - 🔍 預準備語句

附: 各SQL的官方文檔
SQLite MySQL

🔑 鍵 - 值對 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 : 任意类型
    要寫入的數據。允許使用的數據類型有:
    Integer Float String Boolean Array Object
    其中,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指定數據庫所在路徑SQLiteplugins/test.db-
create數據庫不存在是否自動創建SQLitetrue/falsetrue
readonly以只讀模式打開SQLitetrue/falsefalse
readwrite以讀寫模式打開SQLitetrue/falsetrue

執行SQL並獲取結果集

session.query(sql)

  • 參數:
  • sql : String
    要查詢的SQL語句
  • 返回值:查詢的結果(結果集)
  • 返回值類型:Array<Array>
    返回數組的第1行(result[0])為結果集的表頭(列名),剩餘行為結果數據

若查詢結果為: | a | b | | --- | --- | | ll | 233 | | h | 114 |

則用query方法返回值表示為

JSON
[
  ["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:

SQL
-- 单个?表示参数
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:

SQL
-- 单个?表示参数
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 等語句生效)IntegerSQLite MySQL
stmt.insertId獲取該INSERT/UPDATE/REPLACE語句執行後最後一個更改行的行號(關於行號的解釋詳見官方文檔)IntegerSQLite 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()執行語句,否則語句將不會被執行

一個樣例搞懂幾個重載函數
JavaScript
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()

stmt.fetchAll(callback)

  • 參數:
  • callback : Function<bool(Object)>
    回調函數,用於遍歷結果行;在回調函數中返回false可終止遍歷
  • 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
  • 返回值類型:DBStmt

重置當前語句狀態至“待執行”

stmt.reset()

  • 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
  • 返回值類型:DBStmt
  • 注意:本函數不會清除已綁定的參數

重新執行預準備語句

stmt.reexec()

  • 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
  • 返回值類型:DBStmt
  • 注意:本函數是一個便捷函數,等同於執行stmt.reset()和stmt.execute()

清除所有已綁定的參數

stmt.clear()

  • 返回值:處理完畢的語句對象(便於連鎖進行其他操作)
  • 返回值類型:DBStmt

樣例

JavaScript
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]++;
});