我们已经对 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 关系中。