一、背景問題
自nodejs誕生以來出現了一大批的web框架如express koa2 egg等等,前端可以不再依賴后端可以自己控制服務端的邏輯。原來的后端開發同學的陣地前端如今同樣也寫的風生水起,擼起袖子就是干幾周一個項目前端、后端都自己搞定了,那叫一個效率。
雖然各框架都提供了一些自己的接口去簡化CRUD操作,但是還是沒有解決復雜條件查詢、服務端分頁等等問題,導致開發過程中很多開發者還是直接拼接SQL來訪問數據庫。於是我們就想如何讓訪問數據庫變得簡單易用。
二、類庫設計
操作數據庫都可以看作跟數據庫做一次交互,交互傳遞的是數據+命令,我們希望能夠用盡量簡潔的代碼來描述每一次訪問數據庫。所以可以對mysqljs對數據庫操作進行封裝,重新設計訪問數據庫的API。為nodejs訪問mysql數據庫提供強大流暢的api的工具類庫,目標是希望訪問數據庫邏輯都能使用一行代碼完成,讓訪問數據庫變得更加簡單優雅。開源地址:https://github.com/liuhuisheng/ali-mysql-client
1. 初始化配置
初始化如下
const db = new DbClient({
host : '127.0.0.1',
user : 'root',
password : 'secret',
database : 'my_db'
});
2. 構造查詢
- 2.1 查詢單個值
// 查詢單個值,比如下面例子返回的是數字51,滿足條件的數據條數
const result = await db
.select("count(1)")
.from("page")
.where("name", "測試", "like")
.queryValue();
- 2.2 查詢單條數據
// 查詢單條數據,返回的是 result = {id:12, name: '測試頁面', ....}
const result = await db
.select("*")
.from("page")
.where("id", 12) // id = 12
.queryRow();
- 2.3 查詢多條數據
// 查詢多條數據 返回的是 ressult = [{...}, {...}];
const result = await db
.select("*")
.from("page")
.where("name", "測試頁面", 'like') // name like '%測試頁面%'
.queryList();
- 2.4 服務端分頁查詢
// 查詢多條數據(服務端分頁) 返回的是 ressult = {total: 100, rows:[{...}, {...}]};
const result = await db
.select("*")
.from("page")
.where("id", 100, "lt") // id < 100
.queryListWithPaging(3, 20); //每頁 20 條,取第 3 頁
- 2.5 多表關聯查詢
// 多表關聯查詢
const result = await db
.select("a.page_id, a.saga_key")
.from("page_edit_content as a")
.join("left join page as b on b.id = a.page_id")
.where("b.id", 172)
.queryList();
- 2.6 查詢除了支持各種多表join外,當然還支持groupby orderby having等復雜查詢操作
const result = await db
.select("a1 as a, b1 as b, count(c) as count")
.from("table")
.where("date", db.literals.now, "lt") // date < now()
.where("creator", "huisheng.lhs") // creator = 'huisheng.lhs"
.groupby("a1, b1")
.having("count(category) > 10")
.orderby("id desc")
.queryListWithPaging(2); //默認每頁20條,取第2頁
3. 構造插入
const task = {
action: "testA",
description: "desc1",
state: "123",
result: "result1"
};
// 插入一條數據
const result = await db
.insert("task", task)
.execute();
// 也支持直接寫字段,支持增加字段
const result = await db
.insert("task")
.column("action", "test")
.column("create_time", db.literals.now)
.execute();
// 插入多條數據
const tasks = [ task1, taks2, task3 ];
const result = await db
.insert("task", tasks)
.execute();
// 支持增加或覆蓋字段
const result = await db
.insert("task", tasks)
.column('create_time', db.literals.now) // 循環賦值給每一行數據
.column('create_user', 'huisheng.lhs')
.execute();
4. 構造更新
const task = {
action: "testA",
description: "desc1",
state: "123",
result: "updateResult"
};
//更新數據
const result = await db
.update("task", task)
.where("id", 1)
.execute();
//更新數據,支持增加字段
const result = await db
.update("task")
.column("action", "test-id22")
.column("create_time", db.literals.now)
.where('id', 2)
.execute();
5. 構造刪除
//刪除id為1的數據
const result = await db
.delete("task")
.where("id", 1)
.execute();
6. 事務控制
const trans = await db.useTransaction();
try {
// 數據庫操作
// await trans.insert(...)
// await trans.update(...)
await trans.commit();
} catch (e) {
await trans.rollback();
}
7. 復雜條件查詢設計
7.1 查詢條件所有參數說明
// 查詢條件所有參數
const result = await db
.where(field, value, operator, ignore, join) // 支持的所有參數
.where({field, value, operator, ignore, join}) //支持對象參數
.queryList();
// 復雜查詢條件
const result = await db
.select("*")
.from("page")
.where("id", 100, "gt") // id > 100
.where("tags", "test", "like") //name like '%test%'
.where("tech", tech, "eq", "ifHave") // tech='tech_value' 當 tech 為空時,不做為查詢條件
.where("tags", tags, "findinset", "ifHave", "or")
.queryList();
- field 字段名
- value 傳入值
- operator 操作符,默認equal4
- ignore 是否加為條件,返回false時則忽略該條件
- join 連接符號(and or),默認為and
7.2 操作邏輯定義operator
該參數很好理解,默認值為equal,支持傳字符串或傳入函數,傳入字符串則會匹配到已定義的邏輯,
const result = await db
.select("*")
.from("page");
.where("id", 100, "lt") // id < 100
.where("group_code", "dacu") // group_code = "dacu"
.queryList();
大家能理解operator是為拼接查詢條件使用的邏輯封裝,復雜條件的拓展能力都可以靠自定義的operator來完成。其函數的形式如下:
const customOperator = ({ field, value }) => {
if (condition) {
return {
sql: '?? = ?',
arg: [ field, value ],
};
} else {
return {
sql: '?? > ?',
arg: [ field, value ],
};
}
};
// 可直接使用也可注冊到全局
const config = db.config();
config.registerOperator("customOperator", customOperator);
7.3 是否加為條件ignore
這個需要解釋下,當滿足xx條件時則忽略該查詢條件,ignore設計的初衷是為了簡化代碼,比如以下代碼是很常見的,界面上有輸入值則查詢,沒有輸入值時不做為查詢條件:
const query = db
.select("*")
.from("page");
.where("id", 100, "lt");
if (name){
query.where("name", name, 'like');
}
if (isNumber(source_id)){
query.where('source_id', source_id)
}
const result = await query.queryList();
上面的代碼使用ignore時則可簡化為:
const result = await db
.select("*")
.from("page")
.where("id", 100, "lt")
.where("name", name, "like", "ifHave") //使用內置 ifHave,如果name為非空值時才加為條件
.where("source_id", tech, "eq", "ifNumber") //使用內置 ifNumber
.queryList();
支持傳字符串或傳入函數,傳入字符串則會匹配到已定義的邏輯,其函數的形式如下:
const customIgnore = ({field, value}) => {
if (...){
return false;
}
return true;
};
//也可以注冊到全局使用
const config = db.config();
config.registerIgnore("customIgnore", customIgnore);
7.4 查詢條件優先級支持
// where a = 1 and (b = 1 or c < 1) and d = 1
const result = await db.select('*')
.from('table')
.where('a', 1)
.where([
{field: 'b', value: '1', operator:'eq'},
{field: 'c', value: '1', operator:'lt', join: 'or'},
])
.where('d', 1)
.queryList();
7.5 真實場景中的復雜查詢示例
// 復雜查詢,真實場景示例,項目中拓展了keyword、setinset等operator及ignore
const result = await app.db
.select('a.*, b.id as fav_id, c.name as biz_name, d.group_name')
.from('rocms_page as a')
.join(`left join favorite as b on b.object_id = a.id and b.object_type = "rocms_page" and b.create_user = "${this.ctx.user.userid}"`)
.join('left join rocms_biz as c on c.biz = a.biz')
.join('left join rocms_biz_group as d on d.biz = a.biz and d.group_code = a.biz_group')
// 關鍵字模糊查詢
.where('a.name,a.biz,a.biz_group,a.support_clients,a.owner,a.status', query.keywords, 'keywords', 'ifHasValueNotNumber') // 關鍵字在這些字段中模糊查詢
.where('a.id', query.keywords, 'eq', 'ifNumber') // 關鍵字中輸入了數字時當作id查詢
// 精確查詢
.where('a.id', query.id, 'eq', 'ifHave')
.where('a.name', query.name, 'like', 'ifHave')
.where('a.biz', query.biz, 'eq', 'ifHave')
.where('a.biz_group', query.biz_group, 'eq', 'ifHave')
.where('a.support_clients', query.support_clients, 'setinset', 'ifHave')
.where('a.status', query.status, 'insetfind', 'ifHave')
.where('a.owner', query.owner, 'eq', 'ifHave')
.where('a.offline_time', query.owner, 'eq', 'ifHave')
// TAB類型 我的頁面own、我的收藏fav、所有頁面all
.where('a.owner', this.ctx.user.userid, 'eq', () => query.queryType === 'own')
.where('b.id', 0, 'isnotnull', () => query.queryType === 'fav')
// 分頁查詢
.orderby('a.update_time desc, a.id desc')
.queryListWithPaging(query.pageIndex, query.pageSize);
4. 自定義配置
const config = db.config();
// 自定義operator
config.registerOperator('ne', ({ field, value }) => {
return { sql: '?? <> ?', arg: [ field, value ] };
});
// 自定義ignore
config.registerIgnore('ifNumber', ({ value }) => {
return !isNaN(Number(value));
});
// 監聽事件 執行前
config.onBeforeExecute(function({ sql }) {
console.log(sql);
});
// 監聽事件 執行后
config.onAfterExecute(function({ sql, result }) {
console.log(result);
});
// 監聽事件 執行出錯
config.onExecuteError(function({ sql, error }) {
console.log(error);
});
5. 內置的operator及ignore
-
- eq (equal)
- ne (not equal)
- in (in)
- gt (greater than)
- ge (greater than or equal)
- lt (less than)
- le (less than or equal)
- isnull (is null)
- isnotnull (is not null)
- like (like)
- startwith (start with)
- endwith (end with)
- between (between)
- findinset (find_in_set(value, field))
- insetfind (find_in_set(field, value))
- sql (custom sql)
- keywords (keywords query)
-
- ifHave (如果有值則加為條件)
- ifNumber (如果是數值則加為條件)
三、使用示例
在eggjs中可以和egg-mysql一起使用,初始化時支持直接傳入egg-mysql或ali-rds對象,避免重復創建連接池。
// this.app.mysql 為egg-mysql對象
const db = new DbClient(this.app.mysql)
四、項目開源
該項目最初是在自己項目內部使用只發布在內網中,最近整理去除了內網依賴開源到github上,ali-mysql-client:https://github.com/liuhuisheng/ali-mysql-client
該類庫旨在為nodejs訪問mysql數據庫提供強大流暢的api,目標是希望訪問數據庫邏輯都能使用一行代碼完成,讓訪問數據庫變得更加簡單優雅。有任何意見或建議歡迎大家可以隨時反饋給我。