JScript快速开发框架Edk:js2sql

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

相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页