Node.js 的 DAO 框架:bearcat-dao
概述
bearcat-dao 是一個 node.js 基于 SQL mapping 的 DAO 框架。實現了基于 SQL mapping 來對數據結果集進行映射,是一種半自動化的模式,相比較于 O/R mapping 全自動化的模式。 因此,在 bearcat-dao 里,開發者能夠對SQL進行完全的控制,通過SQL來與數據庫打交道并進行性能優化,bearcat-dao 則會把數據結果集映射到 bearcat model 中去。
SQL mapping vs O/R mapping
結構化查詢語言(SQL)已經存在了非常久的時間。自從 Edgar F.Codd 第一次提出“數據可以被規范化為一組相互關聯的表”這樣的思想以來,已經超過35年了。很少有哪一種軟件技術敢聲稱自己像關系數據庫和SQL那樣經受住了時間的考驗。因此,關系數據庫和SQL仍然很有價值,我們可能都曾有這樣的經歷,應用程序的源代碼(經歷了很多版本)隨著時間的流逝最終還是過時了(無法維護下去),但它的數據庫甚至是SQL本身卻仍然很有價值。
O/R mapping 被設計為用來簡化對象持久化工作的,它通過將SQL完全從開發人員的職責中排除來達到這個目的。在O/R mapping中,SQL是給予應用程序中的類與關系數據庫表之間的映射關系而生成的。除了不用寫SQL語句,使用O/R mapping的API通常也比典型的SQL API要簡單很多,但是O/R mapping仍然不是一顆“銀彈”,它并非適用于所有的場景。
一個最主要的問題就是O/R mapping它需要假設數據庫是被恰當的規范化了,如果沒有被恰當規范,這就會給映射帶來許多麻煩,甚至需要繞些彎路,或者在設計時對效率做些折衷。同時,沒有哪一個對象/關系解決方案可以支持每一種數據庫的每一種特性、每一種能力以及設計上固有的缺陷,它們僅僅能做到一個子集,而能做到全集的恰恰則是SQL這個專為數據庫設計的結構化查詢語言
SQL mapping 與 O/R mapping 不同,它不是直接把類映射為數據庫表或者說把類的字段映射為數據庫列,而是把SQL語句與結果(也即輸入和輸出)映射為類。bearcat-dao 在類(model)和數據庫之間建立了一個額外的中間層,這就為如何在類和數據庫表之間建立映射關系帶來了更大的靈活性,使得在不用改變數據模型或者對象模型的情況下改變它們的映射關系成為可能。這個中間層其實就是SQL,通過SQL可以將類(model)與數據庫表之間的關系降到最低。開發者只需要編寫SQL,bearcat-dao 負責在類(model)屬性與數據庫表的列之間映射參數和結果
Model
model 定義使用 bearcat model
因此,可以非常容易的就設置映射關系、約束、relation關系
例如,我們有一個 test 表,它只有一個 id 主鍵字段
create table test( id bigint(20) NOT NULL COMMENT 'id', PRIMARY KEY (id) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后,我們可以定義下面的 model
var TestModel = function() { this.$mid = "testModel"; this.$table = "test"; this.id = "$primary;type:Number"; } module.exports = TestModel;
在 TestModel 里,我們使用 $table 屬性來設置需要映射的表名,對于 id 屬性,我們用 primary 表明這是一個主鍵,并且我們給這個字段添加了一個 type 約束,限定它一定為 Number 類型
Relation
在關系型數據庫的表與表之間是可以有 relation 的,也即關系,有一對一、一對多、多對多這三種情況
一對一 relation
一對一關系意味著兩張表,一張表有另外一張表的id引用(或者外鍵)。在model對象里面就是說,兩個model,是一對一的
比如,我們有兩張表,test1 表有對 test2 表的 id 引用
create table test1( id bigint(20) NOT NULL COMMENT 'id', rid bigint(20) NOT NULL COMMENT 'reference to test2 id', PRIMARY KEY (id) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
create table test2( id bigint(20) NOT NULL COMMENT 'id', PRIMARY KEY (id) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后,我們就可以定義這樣的 model
var Test1Model = function() { this.$mid = "test1Model"; this.$table = "test1"; this.id = "$primary;type:Number"; this.test2 = "$type:Object;ref:test2Model" } module.exports = Test1Model;
var Test2Model = function() { this.$mid = "test2Model"; this.$table = "test2"; this.id = "$primary;type:Number"; } module.exports = Test2Model;
通過用 Test1Model.test2 屬性,我們使用 ref:test2Model 來設置對 test2Model 的引用
一對多 relation
一對多則意味著,一個model引用著另外一個model數組。比如,我們有一個博客,這個博客里面的文章有很多評論,這個博客文章與評論之間的關系就是一對多的
var Test1Model = function() { this.$mid = "test1Model"; this.$table = "test1"; this.id = "$primary;type:Number"; this.test2 = "$type:Array;ref:test2Model" } module.exports = Test1Model;
在上面的model定義中,我們簡單的把 test2 屬性的 type 改成 Array 即可,它就變成了一對多的關系
多對多 relation
多對多一般可以通過中間表,來轉化成兩個一對多的關系
SQL 模板
當編寫復雜sql語句的時候,如果僅僅使用 String 類型的字符串來編寫,肯定非常痛苦,更好的方式是用 SQL 模板
編寫SQL模板相當簡單
比如,我們可以定義 id 為 testResultSql 的 SQL 模板
sql testResultSql select * from test end
然后我們可以在dao中使用這個 SQL 模板
domainDaoSupport.getList("$testResultSql", null, "testModel", function(err, results) { // results is testModel type array });
第一個參數,開頭帶上 $ 就表面是一個 SQL 模板
同時,由于是模板,因此可以包含其他模板,比如
sql testResultSql select * from ${testResultTable} end sql testResultTable test end
這個結果和上面是一樣的
ResultSet 映射
數據庫結果集是一個由field/value對象組成的數組,因此映射結果集就像用特定key/value對來填充對象。為了能夠做到匹配,我們使用 model 屬性值里的 prefix model magic attribute value 或者 model 屬性里的 prefixmodel attribute
比如,如果你查詢得到了如下的 resultSet
[{ "id": 1, "title": "blog_title", "content": "blog_content", "create_at": 1234567, "update_at": 1234567 }]
那么,映射的model就是這樣的
var BlogModel = function() { this.$mid = "blogModel"; this.$table = "ba_blog"; this.id = "$primary;type:Number"; this.aid = "$type:Number"; this.title = "$type:String"; this.content = "$type:String"; this.create_at = "$type:Number"; this.update_at = "$type:Number"; } module.exports = BlogModel;
如果結果集字段是已 ***blog_***開頭,比如
[{ "blog_id": 1, "blog_title": "blog_title", "blog_content": "blog_content", "blog_create_at": 1234567, "blog_update_at": 1234567 }]
那么,映射model就是這樣的
var BlogModel = function() { this.$mid = "blogModel"; this.$table = "ba_blog"; this.$prefix = "blog_"; this.id = "$primary;type:Number"; this.aid = "$type:Number"; this.title = "$type:String"; this.content = "$type:String"; this.create_at = "$type:Number"; this.update_at = "$type:Number"; } module.exports = BlogModel;
僅僅需要添加 this.$prefix model 屬性
DAO
DAO 是領域對象模型的縮寫,一般用于操作數據庫
bearcat-dao 提供 domainDaoSupport 對象,封裝了基本的sql、cache操作。使用它也非常簡單,直接依賴注入,然后通過 init 方法進行初始化
simpleDao.js
var SimpleDao = function() { this.$id = "simpleDao"; this.$init = "init"; this.$domainDaoSupport = null; } SimpleDao.prototype.init = function() { // init with SimpleModel id to set up model mapping this.domainDaoSupport.initConfig("simpleModel"); } // query list all // callback return mapped SimpleModel array results SimpleDao.prototype.getList = function(cb) { var sql = ' 1 = 1'; this.$domainDaoSupport.getListByWhere(sql, null, null, cb); } module.exports = SimpleDao;
完整的api可以參見 domainDaoSupport
配置使用
修改項目中的context.json placeholds 可以很方便的在不同環境間切換
"dependencies": { "bearcat-dao": "*" }, "beans": [{ "id": "mysqlConnectionManager", "func": "node_modules.bearcat-dao.lib.connection.sql.mysqlConnectionManager", "props": [{ "name": "port", "value": "${mysql.port}" }, { "name": "host", "value": "${mysql.host}" }, { "name": "user", "value": "${mysql.user}" }, { "name": "password", "value": "${mysql.password}" }, { "name": "database", "value": "${mysql.database}" }] }, { "id": "redisConnectionManager", "func": "node_modules.bearcat-dao.lib.connection.cache.redisConnectionManager", "props": [{ "name": "port", "value": "${redis.port}" }, { "name": "host", "value": "${redis.host}" }] }]
如果你不需要使用redis, 你可以移除redisConnectionManager定義
事務
bearcat-dao 基于 bearcat AOP 提供了事務支持. aspect 是 transactionAspect , 提供了 around advice, 當目標事務方法調用cb函數的時候傳入了 err, rollback 回滾操作就會被觸發, 相反如果沒有cb(err)的話, 事務就會被提交(commit).
pointcut 定義的是:
"pointcut": "around:.*?Transaction"
因此, 任何已 Transaction 結尾的POJO中的方法都會匹配到 transaction 事務
由于transaction必須在同一個connection中, 在 bearcat-dao 中是通過 transactionStatus 來保證的, 在同一個事務的 transaction 必須在同一個transactionStatus中
SimpleService.prototype.testMethodTransaction = function(cb, txStatus) { var self = this; this.simpleDao.transaction(txStatus).addPerson(['aaa'], function(err, results) { if (err) { return cb(err); // if err occur, rollback will be emited } self.simpleDao.transaction(txStatus).getList([1, 2], function(err, results) { if (err) { return cb(err); // if err occur, rollback will be emited } cb(null, results); // commit the operations }); }); }
開啟 Debug 模式
跑node應用時帶上BEARCAT_DEBUG為true
BEARCAT_DEBUG=true node xxx.js
開啟debug模式后,就能看到具體執行SQL的日志