我们已经对 ORM 的概念耳熟能详了,而且使用动态语言一般都能比较轻松地写一套 ORM 方案,网上比比皆是,有的参照 Ruby on Rail 的 ActiveRecord,有的参照 iBatis 的 SQLTemplate 模式,还有的采用微软 Linq 的方案,无论哪一种思想和模式都力求把数据的增、删、改、查的操作映射(Mapping)为一套 API,用面向对象(Object-Oriented)的方式将这些对象组织起来,代替繁复的 SQL 来实现系统业务逻辑的过程,当然,最终仍要转换到数据库可以解析的 SQL 语句。“映射、业务对象、数据库”这三者之间,映射负责业务逻辑到数据持久化的过程,并且可以想像映射居于业务对象与数据库的“中间位置”。业务对象不但不会与数据库直接打交道,而且其结果就是要让业务对象与数据库(数据库)的分离。中间层的映射、如何映射于是显得相当重要,直接影响到整体架构上的解耦。这里又提到了 ORM 的一点好处:“解耦”。大多分离的目的便是为了解耦,看怎么“解”。如果深入更多的 ORM 好处是什么就让大家去感受了,或者开发过程中大家都明白的,熟悉的,天天打交道的,本文则不想罗嗦复述。一言蔽之,当为称道的地方乃解放了生产力——此说法可谓最适用不过。不过,看官会不会觉得行文至此写得比较笼统?为表示笔者做了功课,还是贴一帖人家总结的现象,说明 ORM 带来的好处:
目前大多数项目或产品都使用关系型数据库实现业务数据的存储,这样在开发过程中,常常有一些业务逻辑需要直接用写SQL语句实现,但这样开发的结果是:遍地布满SQL语句。这些高藕合的SQL语句给系统的改造和升级带来很多无法预计的障碍。为了提高项目的灵活性,特别是快速开发,ORM是一个不错的选择。举个简单的例子:在使用ORM的系统中,当数据库模型改变时,不再需要理会逻辑代码和SQL语句中涉及到该模型的所有改动,只需要将该模型映射的对象稍作改动,甚至不做改动就可以满足要求。(1)
如果 NoSql、对象数据库流行后,那么则可能不需要存在映射层。当前的 ORM 实质可被认为是介乎关系模型和对象模型之间的一种混合协调机制。至于什么“O/R 阻抗失衡(O/R Impedance Mismatch)”、“OO 数据库”、“使用图模型分析很多对象间复杂关系的设计合理性”,俺水平所限,不能也不可能深究,总之简单的说,不直接写 SQL 就对了。
我们知道虽然数据库都支持标准 SQL 语句,但不同的数据库之间还是会存着着不少差异,这导致我们在操作不同的数据库时不能统一 SQL 语句, 一旦更换了数据库,那意味着 SQL 要重写。为了避免灾难性的后果,我们就需要对数据进行封装。 封装的意义就在于不管数据库如何变化,我们都只需要使用相同的代码去获取和构建我们需要的数据结构,而不用关心不同数据库之间对于 SQL 的支持不同而造成的差异。
下面是我尝试用 JS 写的动态生成 SQL 类。
$$.sql.Parser = function(){
var data = this.data;
if(!data) {
data = this.data = {};
}
data.mainTable = '';
data.top = {};
data.top.value = 0;
data.top.sql = '';
data.field = {};
data.field.value = [];
data.field.sql = '';
data.subQuery = {};
data.subQuery.value = [];
data.subQuery.sql = '';
data.join = {};
data.join.value = [];
data.join.sql = '';
data.filter = {};
data.filter.value = [];
data.filter.sql = '';
data.orderBy = {};
data.orderBy.value = [];
data.orderBy.sql = '';
data.noConflict = {};
data.noConflict.sql = '';
// for Top(First)
this.setTop = function(num){
this.data.top.value = num
this.data.top.sql = "TOP " + num + ' ';
return this;
}
this.setField = function(field){
var _field = this.data.field;
// 如果前面有字符串却没有 分号,则自动加上一个。
if(_field.value.length && /(?!,)\s?$/.test(_field.sql)){
_field.sql += ',';
}
_field.value.push(field);
_field.sql += ' '+ field + ' ';
return this;
}
this.addField = function(){
var fields = [];
for(var i = 0, j = arguments.length; i < j; i++){
fields.push(arguments[i].getSql().alias);
}
fields = fields.join(', ');
this.setField(fields);
}
// for fields
this.setSubQuery = function(queristInstance, token){
this.data.subQuery.value.push(queristInstance);
this.data.subQuery.sql += ',(' + queristInstance.getQuerySql() + ') AS ' + token;
return this;
}
/**
* 设置主题表格。
* @param {String} mainTable
*/
this.setMainTable = function(mainTable){
this.data.mainTable = mainTable;
return this;
}
this.setTable = this.setMainTable; // shorthand
/**
* for tables
* 生成下面的语句:
* USER] INNER JOIN (news INNER JOIN newsClass ON news.ClassID = newsClass.ID) ON USER.ID = news.SubmitterID;
* 有两个关系:
* student(s#,sname,d#),即学生这个关系有三个属性:学号,姓名,所在系别
* dep(d#,dname),即院系有两个属性:系号、系名则s#、d#是主键,也是各自所在关系的唯一候选键,d#是student的外键。
*/
this.setJoin = (function(){
var tpl = "({1} INNER JOIN {0} ON {3}.{4} = {1}.{2})";
/**
* @param {String} mainTable
* @param {Array} tables
* @return {String}
*/
function makeJoin(mainTable, tables){
var
join
,table
,inner // 内面一层的join
,out = []; // Is this useful? @todo
for(var i = 0, j = tables.length; i < j; i++){
table = tables[i]
,inner = (i == 0) ? mainTable : out[i - 1]
,join = tpl.format(
inner
,table.tableName
,table.primaryKey
,mainTable
,table.foreignKey
);
out.push(join);
// SELECT fields, noConflict
if(table.noConflict){
var noConflict = new String(' ');
for(var q = 0, p = table.noConflict.length; q < p; q++){
noConflict += ',' + table.noConflict[q] + ' ';
}
this.data.noConflict.sql = this.data.noConflict.sql + noConflict;
}
}
return join;
}
return function(mainTable, tables){
var thisJoin = this.data.join;
// 保存value,就是保存參數
thisJoin.value.push([ mainTable, tables]);
// 不用join,沒有tables
if(!tables){
thisJoin.sql = mainTable;
return thisJoin.sql;
}
// 如果有sql值存在,表示這是第二次調用join(),則mainTable為fn.sql
if(thisJoin.sql && thisJoin.sql != ''){
mainTable = thisJoin.sql;
}
thisJoin.sql = makeJoin.call(this, mainTable, tables); // 每次join操作都会破坏旧字符串
return thisJoin.sql;
}
})()
this.addJoin = function(join){
var tmpJoin = this.data.tmpJoin;
if(!tmpJoin){
tmpJoin = this.data.tmpJoin = [];
}
tmpJoin.push(join);
return this;
}
// for where
this.setFilter = function(fieldObj, operator, value){
switch(fieldObj.type){
case String:
value = String(value);
value = "'" + $$.trim(value) + "'";
break;
}
var filterSql = fieldObj.toString({fullFieldName : true}) + ' ' +operator + ' ' + value;
this.data.filter.value.push(filterSql);
this.data.filter.sql += ' '+ filterSql + ' ';
return this;
}
// for order
this.orderBy = function(fieldObj, sort){
var sql = ' ORDER BY {0} {1} ';
this.data.orderBy.sql = sql.format(fieldObj.toString({fullFieldName : true}), sort);
return this;
}
// for page
this.limit = function(){
return emptyStr;
return this;
}
this.getQuerySql = function(isForPage){
var data = this.data;
// 先有 tables,然后才有 data.noConflict
var tables; // 关联表
// 为防止影响data.join.sql,所以先有一判断
tables = data.join.sql || this.setJoin(data.mainTable, data.tmpJoin);
var fields; // 字段
if(isForPage){
fields = this.data.mainTable + '.uid';
}else{
fields = '';
fields += data.field.sql;
fields += data.subQuery.sql;
fields += data.noConflict.sql;
fields = fields || '*';
}
var filter = data.filter.sql; // 查询条件
if( filter && isForPage != false){/* 分页之后 filter 无效 */
filter = ' WHERE ' + filter;
}else{
filter = '';
}
var order = data.orderBy.sql; // 排序
var top = data.top.sql; // first
return "SELECT " + top + fields + ' FROM ' + tables + filter + order;
}
// For write action
/**
* @param {Object} entityObj
* @param {Action} action
* @return {String} SQL语句。
*/
function write(entityObj, action){
var sql;
switch(action){
case $$.data.INITIALIZE: // 新建表格。
var insertData = entityObj.eachFields(entity2insert, [ [], [] ]);
sql = sqlTpl_Insert.format(
entityObj.tableName
,insertData[0].join(',')
,json2sql(insertData[1].join(','))
);
break;
case $$.data.CREATE: // 执行SQL INSERT指令。
var insertData = entityObj.eachFields(entity2insert, [ [], [] ]);
sql = sqlTpl_Insert.format(
entityObj.tableName
,insertData[0].join(',')
,json2sql(insertData[1].join(','))
);
break;
case $$.data.UPDATE: //
break;
case $$.data.DELETE: // 输入记录的id参数,删除记录。
sql = sqlTpl_Delete.format(entityObj.tableName, entityObj.id);
break;
default:
// warn
break;
}
return sql;
}
/**
* @this {Array}
*/
function entity2insert(field){
this[0].push(field.key);
this[1].push(field.getSqlValue());
}
/**
* 送入一个JSON值,根据其类型返回符合SQL存储的字符串。
* @private
* @param {Any} v 值
* @return {String} 符合sQL存储的字符串。
*/
function json2sql(v){
switch(typeof(v)){
case 'boolean':
break;
case 'string':
v = v.replace(/\'/g, "''").replace(/\r/g,"").replace(/(wh)(ere)/ig, "$1'+'$2");
v = "'"+ v +"'";
break;
case 'number':
/*
* Access 双精度数字需要转换
* @dep
if(false){
v = " Format(" + v + ",'#0.0000') ";
}*/
break;
// if (value && (typeof value.getFullYear == 'function')) {// 日期类型转换
// value = "#" + value.format("Y-m-d h:i:s") + "#";
// }
case 'object':
if(v.toString() === new Date(v).toString()){ // 日期类型
v = (
$$.cfg.edk_dbType == 'mysql'
? "'" + v.format("yyyy-mm-dd HH:MM:ss") + "'"
: "#" + v.format("yyyy-mm-dd hh:MM:ss") + "#"
);
break;
}
default :
throw "unknow type!";
}
return v;
}
function json2sql(type, value){
var str;
switch(type){
case Boolean:
case Number:
var isAccesss = true; // @todo get this config from &&.cfg
// Access 双精度数字需要转换
if(String(value).length > 10 && isAccesss){
value = " Format({0},'#0.0000') ".format(value);
}
str = value;
break;
case String:
str = String(value + '');
str = str.replace(/\'/g, "''"); // @todo is this necessary ?.replace(/\r/g,"").replace(/(wh)(ere)/ig, "$1'+'$2");
str = "'"+ str +"'";
break;
// case Array:
// break;
// case Function:
// break;
// case Object:
// break;
case Date:
// mysql "'" + v.format("yyyy-mm-dd HH:MM:ss") + "'"
str = "#" + value.format("yyyy-MM-dd hh:mm:ss") + "#"
break;
// case RegExp:
// break;
case 'undefined':
case null:
str = 'NULL';
break;
default:
throw '未解析的字段';
}
return str;
}
/**
* 这里一定要转换,因为COM日期类型到JS中很奇怪。
* 原理ADO Types -->JS Types
* 以字符出现date字眼为判断!
* @param {String} key
* @param {COM/COM+} v
* @return {Date}
*/
function getPrimitiveDate(key, v){
if(/date/i.test(key)){
v = new Date(v);
}
return v;
}
var
insertSql = 'INSERT INTO {0} ({1}) VALUES ({2})'
,updateSql = 'UPDATE {0} SET {1} WHERE uid = {2}'
,deleteSql = 'DELETE FROM {0} WHERE uid = {1}';
/**
* 返回执行 SQL CREATE 指令。
* @param {Object} data
* @return {String}
*/
this.getInsertSql = function(data){
var fields = [], values = [];
for(var i in data){
fields.push(i);
values.push(json2sql(data[i][0], data[i][1]));
}
fields = fields.join(', ');
values = values.join(', ');
var sql = insertSql.format(this.data.mainTable, fields, values);
return sql;
};
/**
* 返回执行 SQL UPDATE 指令。
* @param {Number} uid
* @param {Object} data
* @return {String}
*/
this.getUpdateSql = function(uid, data){
var sql = [];
for(var i in data){
sql.push(i + ' = ' + json2sql(data[i][0], data[i][1]));
}
sql = sql.join(',');
sql = updateSql.format(this.data.mainTable, sql, uid.toString());
return sql;
}
this.getDelSql = function(entry){
};
/**
* 新建表格。
* @param {Object} entityObj
* @param {Boolean} isDropTable 创建表格之前是否删除表格。
* @return {Boolean} True表示创建成功。
*/
this.getCreateTableSql = function(entityObj, isDropTable){
var
sql = 'CREATE TABLE ' + tableName
,meta = $$.Model.meta
,fieldName
,SQLType
,arr = [];
for(fieldName in model){
SQLType = meta.getMeta.call(model[fieldName], meta.SQLType);
switch(SQLType){
case 'BOOLEAN':
SQLType = ($$.cfg.edk_dbType == 'access') ? 'BIT' : SQLType;
break;
case 'CHAR':
SQLType = 'CHAR({0})'.format(meta.getMeta.call(model[fieldName], meta.Length))
break;
}
arr.push(fieldName + ' ' + SQLType + (fieldName == "ID" ? " NOT NULL PRIMARY KEY" : ""));
}
sql += '('+ arr.join('\n,') + ")";
if(isDropTable){
// Access不支持 IF EXIST……
$$.sql.execute('DROP TABLE {0}'.format(tableName));
}
//进行数据库操作
return sql;
}
};
后来,我了解了 ROR,于是也学着搞点 ActiveRecord。
预备知识:ActiveRecord 模式
数据层采用 ActiveRecord 模式关联数据(Associations)。 最经典的 ActiveRecord 应用是出现在知名 Web 框架 Ruby on Rails (RoR)1上,它使得 RoR 的 ORM2 建模方案有快速、灵活等的优点。关于 ActiveRecord 模式深度了解,可以参阅权威文献“Active Record(http://www.martinfowler.com/eaaCatalog/activeRecord.html)”,作者 Martin Fowler。
怎么认识 ActiveRecord 模式呢?它是 ORM 方案中的一种,使得我们可以以对象的方式处理关系型数据库为“行、列”为单位的处理方式。这里不打算深入探讨背景的机制,就简单地讲一下基本认识。
在分析的时候,要定义不同对象之间的关系是非常自然的。比如说,在一个食谱数据库中,一条食谱可能会有多条评论,多条评论又可能为同一作者所写,而作者又可以创造多条食谱。透过定义这种链接到一起的关系可允许你通过更为直观的、更强大的方式去操纵数据,——而这种方式,就是 ActiveRecord 的关系链接来支持。
首先是 belongTo 关联。何谓“belongTo”,我们不妨这样说(“属于”这里表示特定的关系):
- 公司数据库中,账单 accout 属于 公司 company;
- 论坛程序中,帖子 thread 属于论坛 forum,也属于分类 cateory;
- 一个画册中,缩略图 thumbnail 属于picture。
如果 bar 属于foo,也就是说它们之间的关系 belongTo,那么一般情况下,关系型数据库物理表中,bar 表会有一称作 foo_id 的字段,作为外键(foreign_key)出现。相对地,与 belongs_to 关联相对应地就是 hasMany 关联,也就是“多对一” v.s “一对多”之间的区别;
也许我们可以借助“父-子”的概念去理解,例如父母可有多个孩子,孩子只有一对父母。账单相当于公司是“子级别”,而公司相当于账单是“父级别”。
当前可支持 belongTo(多对一)、hasMany(一对多)、多对多。而 hasOne 关系实际上包含在 belongTo 关系中。