概述
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 主键字段
1
2
3
4
5
|
create
table
test(
id
bigint
(20)
NOT
NULL
COMMENT
'id'
,
PRIMARY
KEY
(id)
)ENGINE=InnoDB
DEFAULT
CHARSET=utf8;
|
然后,我们可以定义下面的 model
1
2
3
4
5
6
7
|
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 引用
1
2
3
4
5
6
|
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;
|
1
2
3
4
5
|
create
table
test2(
id
bigint
(20)
NOT
NULL
COMMENT
'id'
,
PRIMARY
KEY
(id)
)ENGINE=InnoDB
DEFAULT
CHARSET=utf8;
|
然后,我们就可以定义这样的 model
1
2
3
4
5
6
7
8
|
var
Test1Model =
function
() {
this
.$mid =
"test1Model"
;
this
.$table =
"test1"
;
this
.id =
"$primary;type:Number"
;
this
.test2 =
"$type:Object;ref:test2Model"
}
module.exports = Test1Model;
|
1
2
3
4
5
6
7
|
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 模板
1
2
3
|
sql testResultSql
select
*
from
test
end
|
然后我们可以在dao中使用这个 SQL 模板
1
2
3
|
domainDaoSupport.getList(
"$testResultSql"
,
null
,
"testModel"
,
function
(err, results) {
// results is testModel type array
});
|
第一个参数,开头带上 $ 就表面是一个 SQL 模板
同时,由于是模板,因此可以包含其他模板,比如
1
2
3
4
5
6
7
|
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
1
2
3
4
5
6
7
|
[{
"id"
: 1,
"title"
:
"blog_title"
,
"content"
:
"blog_content"
,
"create_at"
: 1234567,
"update_at"
: 1234567
}]
|
那么,映射的model就是这样的
1
2
3
4
5
6
7
8
9
10
11
12
|
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_***开头,比如
1
2
3
4
5
6
7
|
[{
"blog_id"
: 1,
"blog_title"
:
"blog_title"
,
"blog_content"
:
"blog_content"
,
"blog_create_at"
: 1234567,
"blog_update_at"
: 1234567
}]
|
那么,映射model就是这样的
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 可以很方便的在不同环境间切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
"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 定义的是:
1
|
"pointcut"
:
"around:.*?Transaction"
|
因此, 任何已 Transaction 结尾的POJO中的方法都会匹配到 transaction 事务
由于transaction必须在同一个connection中, 在 bearcat-dao 中是通过 transactionStatus 来保证的, 在同一个事务的 transaction 必须在同一个transactionStatus中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
1
|
BEARCAT_DEBUG=
true
node xxx.js
|
开启debug模式后,就能看到具体执行SQL的日志