Sofire Suite 是一套个人从 2009 年 08 月开始着手研发的套件。历经几年的不断优化改进,从最初的 V 套件到 Sofire2011 到目前的 Sofire.v1.0,Sofire 已经经历了许多项目的考验,并且出色的完成它的使命。现在,我将这套组件再次重构,尝试让它成为任意平台、框架、套件的的底层首选。秉着开源精神,希望这套组件在博友的讨论中不断成长、成熟。
本文主要介绍是概念版的—— SOFIRE XML SQL,我们称之为 X-SQL。
目录
前两个礼拜,某个同事在公司内部,举办了一场小型培训会。他给大家绍 JAVA 的一个开源套件——MyBatis(百度百科) 。
关于 Mybatis 含有一个非常重要的功能,就是 XML 化 T-SQL 语句(也成为动态 T-SQL)。它彻底的将 T-SQL 语句从代码层上剥离,最大程度的动态化,也提高了产品的可维护性。
或许,这只是 Mybatis 的功能之一,我并没有对它的其他功能进行深入研究。不过我却对它的设计理念充满兴趣,培训会结束后,我便搜寻有基于 .NET 版的 Mybatis。
Mybatis.NET,总体来说还算不错,但它有一些与 JAVA 版的 Mybatis 不同,比如在条件判断上他并不是采用 "test" 属性进行验证,而是通过条件 XML 元素(如 isNotNull XML 元素之类)进行判定。
<insert id="InsertAccountDynamic" parameterClass="Account"> INSERT INTO Accounts (Account_ID, Account_FirstName, Account_LastName, Account_Email) VALUES(#Id#, #FirstName#, #LastName# <dynamic prepend=","> <isNotNull prepend="," property="EmailAddress"> #EmailAddress# </isNotNull> <isNull prepend="," property="EmailAddress"> null </isNull> </dynamic> ) </insert>
<update id="updateStudent_if_set" parameterType="liming.student.manager.data.model.StudentEntity"> UPDATE STUDENT_TBL <set> <if test="studentName != null and studentName != '' "> STUDENT_TBL.STUDENT_NAME = #{studentName}, </if> </set> WHERE STUDENT_TBL.STUDENT_ID = #{studentId}; </update>
从上面的 XML 可以看出,Mybatis.NET 的 XML 表达式判定语法显得十分臃肿,在多条件(如“userid=1 or userid=2”)无法友好的展示。所以……我的臭毛病又爆发了——“造轮子”。
好吧,我承认我的轮子已经造的过多了……
从整体功能方面,我尽可能的参考 Mybatis,但并不会失去 X-SQL 本身存在的特点(高效什么等,我就不扯蛋了):
0、关键字(所有关键字、参数、函数均匹配大小写) and、or、not、true、false、null、if、E(检查一下) 1、基本数据类型(Integer、String、DateTime、Float、Boolean) 2、支持 x:数字 s:字符串 d:日期 o:任意类型 数学函数 :Abs(x)、Acos(x)、Asin(x)、Atan(x)、Ceiling(x)、Cos(x)、Exp(x)、Floor(x) :IEEERemainder(x1,x2)、Log(x1,x2)、Log10(x)、Pow(x1,x2)、Round(x1,x2)、Sign(x) :Sin(x)、Sqrt(x)、Tan(x)、Truncate(x)、Max(x1,x2)、Min(x1,x2)、Floor(x) 字符串函数 :isEmpty(s)、len(s)、lower(s)、upper(s) :trim(s)、ltrim(s)、rtrim(s) :contains(s1,s2)、left(s1,s2)、right(s1,s2) :indexOf(s1,s2,[index],[count])、lastIndexOf(s1,s2,[index],[count]) :substr(s1,index,[length])、remove(s1,index,[length]) :replace(s1,s2,s3) :regexMatch(input,pattern,'[mir]') :regexSplit(input,pattern,'[mir]') :regexReplace(input,pattern,replacement,'[mir]') :concat(arg1,arg2,arg...,argN) 日期函数 :date([year],[month],[day],[hour],[minute],[second]) :year(d)、month(d)、day(d)、hour(d)、minute(d)、second(d)、millisecond(d) :dayOfWeek(d)、dateIn(d,beginDate,endDate)、dateAdd(d,x) 转换函数 :toDate(s)、toInteger(s)、toFloat(s)、toString(o,[format])、toBoolean(s) 其他函数 :newGuid('N/D/B/P') :if(boolean,trueValue,falseValue) :in(o,arg1,arg2,arg...,argN) :isnull(o,defaultValue) 3、支持参数/函数的(预)定义,字符串拼接(+), 4、支持数字、浮点数、文本('string')、日期格式(#2012-01-01#) 5、支持以下运算符(一元、二元、三元): ! != % && & ( ) * + , - / : < << <= <> = == > >= >> ? ^ and not or | || ~
<?xml version="1.0" encoding="utf-8" ?> <document> <xsql name="selectAllStudent" cache="forever"> SELECT STUDENT_ID ,STUDENT_NAME ,STUDENT_SEX ,STUDENT_BIRTHDAY ,STUDENT_PHOTO FROM DEMO_STUDENT </xsql> </document>
<?xml version="1.0" encoding="utf-8" ?> <document> <xsql name="selectAllStudent" cache="forever"> SELECT STUDENT_ID ,STUDENT_NAME ,STUDENT_SEX ,STUDENT_BIRTHDAY ,STUDENT_PHOTO FROM DEMO_STUDENT </xsql> <xsql name="test_if"> <include path="selectAllStudent" /> <trim prefix="WHERE" trim="AND|OR"> <if test="studentID==1"> AND STUDENT_ID IN (1,2) </if> <else> AND STUDENT_ID=#{studentID} </else> <if test="studentName != null"> AND STUDENT_NAME=#{studentName} </if> </trim> </xsql> </document>
<?xml version="1.0" encoding="utf-8" ?> <document> <xsql name="selectAllStudent" cache="forever"> SELECT STUDENT_ID ,STUDENT_NAME ,STUDENT_SEX ,STUDENT_BIRTHDAY ,STUDENT_PHOTO FROM DEMO_STUDENT </xsql> <xsql name="test_switch" cache="values"> <include path="selectAllStudent" /> <switch test="studentName"> <case test="=='张三'"> STUDENT_ID=1 </case> <case test="=='李四'"> STUDENT_ID=2 </case> <case test="=='王五'"> STUDENT_ID=3 </case> <default> STUDENT_ID IS NOT NULL </default> </switch> </xsql> </document>
<?xml version="1.0" encoding="utf-8" ?> <document> <xsql name="selectAllStudent" cache="forever"> SELECT STUDENT_ID ,STUDENT_NAME ,STUDENT_SEX ,STUDENT_BIRTHDAY ,STUDENT_PHOTO FROM DEMO_STUDENT </xsql> <xsql name="test_foreach" cache="names"> SELECT * FROM STUDENTS <trim prefix="WHERE" trim="AND|OR"> <if test="studentSex!=null"> AND STUDENT_SEX=#{studentSex} </if> <if test="studentNames!=null"> AND STUDENTNAME IN( <foreach var="studentName" in="studentNames" trim=","> ,#{studentName} </foreach> ) </if> </trim> </xsql> </document>
<xsql name="insert_XML"> <parameters> <xsql name="student_id" cache="forever">select studentPKSequence.nextVal from dual</xsql> <expression name="student_id3">student_id+1</expression> </parameters> INSERT INTO STUDENTS(STUDENT_ID,STUDENT_NAME) VALUES(#{student_id,decimal},#{name,string}) </xsql>
我整理了 12 个实例。你可以通过以下 gif 小动画来查看效果。
虽然我只进行了简单的测试,但 X-SQL 的性能还是十分理想的。在 foreach(该元素最消耗性能) 2000 次测试下,采用缓存是 200~300 毫秒,非缓存效果下 500~600 毫秒(非最佳效果,采用 release,但是在VS 运行测试,而不是直接打开)。
在大多数情况下,我们建议采用 names(根据参数集合长度、参数名进行缓存)以及 forever(永久缓存)。而不建议使用 values(根据参数值缓存)。
其实不一定要采用缓存,通过上面的数据可以看出,缓存的后的性能只有一倍之差(缓存的性能损耗:Dict.TryGetValue,可见 X-SQL 的高性能),如果在非硬性要求下,除了 forever 缓存,其他缓存均不推荐。
压力测试的必要性还是有的,能否经历的起大项目的高频率、多线程调用,我只能表示“我有信心”。
目前我将主要精力集中在:如何实现功能。对于性能等其他的要求,我只能延后。
关于源码
我很犹豫要不要开放出去,其实从之前的文章大家可以看得出,我秉着开源原则,所以开源是必然,但文章发表后为什么不放上源码,主要有两个考虑:
如果有兴趣的朋友很多的话,我会开放到 CodePlex。
最后,对博友 blue1000、 hpze2000 以及其他朋友说一声抱歉,由于最近公司正在着手新产品的研发,我负责的文档太多了,所以一直耽搁下来了。
但我很高兴地告诉你们,关于 Sofire.DataComm.Remoting 以及 Sofire.DataComm.Net.Async 模块已经完成,并且新增了许多功能!系列文章一定会写出来的。
Sofire.DataComm下个版本计划(v2.0)
1、检查高并发下是否存在死锁(理论上暂时不存在),但在万并发量出现死顿(怀疑是本地网络问题?)。
2、增加 buffer 发送前和接收后的(加解密)和(解压缩)。
************************* 2012-07-18 *************************
1、修复 ThreadPool.QueueUserWorkItem 在高频发线程发生阻塞的问题。
2、增加属性【ServiceVersion】,表示服务契约的版本信息,减少客户端连接时返回契约数据。
3、增加属性【SessionExpiration】,表示会话的最大超时时间。小于 0 则表示无超时,等于 0 表示不启用。大于 0 则表示指定的超时秒数。默认为“20”秒。
4、优化内部代码。
5、v1.5 版功能已完成。
我写的很用心,如果您对这篇文章感兴趣的话,请推荐……感谢!
紧接上篇内容《Sofire v1.0 开源——WinForm/SL/WebForm 的 Remoting(1)》。
本文主要讲述 SOFIRE 框架在底层开发中,采用 Sofire.Data 对各种数据库的操作进行统一使用。它仿佛就是对 ADO.NET 的全面封装。
当然,Sofire.Data 支持哪些数据库,关键是在于实现,而不是在于“支持不支持”,理论上所有基于 ADO.NET 的数据库全部支持,而其他数据库,也有部分支持。
当前所支持的数据库包括:MSSQL、Oracle、Oracle、DDTekOracle、OleDb 和 SQLite。至于其他的数据库(如 MySql),由于本人实际项目中并没使用这些数据库,所有暂时不支持,如果你想支持其他数据库,请继承 Sofire.Data.QueryEngineBase。
public DataTable GetTotalReport(string username, DateTime begin, DateTime end) { //处理过程... return new DataTable(); }
当然,这并不是指责这样的写法的好坏,而是建议对函数的返回值进行适当的封装描述。例如,我可以这样:
public Result<DataTable> GetTotalReport(string username, DateTime begin, DateTime end) { try { //处理过程... return new DataTable(); } catch(Exception ex) { return ex; } }
通过这样的约定,可以明确的告诉函数调用者,这个函数返回一个值,但这个操作函数也可能会返回一个错误的内容。
当遇到具有返回内容的操作函数时,可以这样的处理返回结果
public void Test() { var r = this.GetTotalReport("a", DateTime.Now, DateTime.Now); if(r.IsSucceed) { DataTable table = r.Value; } else { Exception ex = r.Exception; } }
简单的代码,表述了返回值具备了函数操作结果的“正确性”,同时也提供了错误的详细信息。以下是返回结果的接口(IResult):
/// <summary> /// 表示一个结果(接口)。 /// </summary> public interface IResult { #region Properties /// <summary> /// 获取执行时发生的错误。 /// </summary> Exception Exception { get; } /// <summary> /// 获取一个值,表示执行结果是否为失败。 /// </summary> bool IsFailed { get; } /// <summary> /// 获取一个值,表示执行结果是否为忽略。 /// </summary> bool IsIgnored { get; } /// <summary> /// 获取一个值,表示执行结果是否为成功。 /// </summary> bool IsSucceed { get; } /// <summary> /// 获取执行的状态。 /// </summary> ResultState State { get; } #endregion Properties #region Methods /// <summary> /// 指定错误信息,将当前结果切换到失败的状态。 /// </summary> void ToFailded(Exception exception); /// <summary> /// 指定错误信息,将当前结果切换到失败的状态。 /// </summary> void ToFailded(string exceptionMessage); /// <summary> /// 将当前结果切换到忽略的状态。 /// </summary> void ToIgnored(); /// <summary> /// 将当前结果切换到成功的状态,并且清除结果的错误信息。 /// </summary> void ToSuccessed(); /// <summary> /// 指示一个结果,与当前结果进行校验。如果 <paramref name="result"/> 是一个错误结果,那么当前的结果将会转换为错误状态。否则将会转换 <paramref name="result"/> 的状态。 /// </summary> /// <param name="result">比较的结果。</param> /// <returns>返回一个值,表示结果是否为成功状态。</returns> bool Checking(Result result); #endregion Methods } /// <summary> /// 表示包含一个返回值的结果(接口)。 /// </summary> /// <typeparam name="TValue">返回值的类型。</typeparam> public interface IResult<TValue> : IResult { #region Properties /// <summary> /// 设置或获取结果的返回值。若结果不处于成功的状态,将返回默认值。 /// </summary> TValue Value { get; set; } #endregion Properties #region Methods /// <summary> /// 将当前结果切换到成功的状态,并且清除结果的错误信息。 /// </summary> /// <param name="value">结果返回的值。</param> void ToSuccessed(TValue value); /// <summary> /// 指示一个结果,与当前结果进行校验。如果 <paramref name="result"/> 是一个错误结果,那么当前的结果将会转换为错误状态。否则将会转换 <paramref name="result"/> 的状态。 /// </summary> /// <param name="result">比较的结果。</param> /// <param name="value">状态为成功时的返回值。</param> /// <returns>返回一个值,表示结果是否为成功状态。</returns> bool Checking(Result result, TValue value); #endregion Methods } /// <summary> /// 表示包含两个返回值的结果(接口)。 /// </summary> /// <typeparam name="TValue1">第一个返回值的类型。</typeparam> /// <typeparam name="TValue2">第二个返回值的类型。</typeparam> public interface IResult<TValue1, TValue2> : IResult { #region Properties /// <summary> /// 设置或获取结果的第一个返回值。若结果不处于成功的状态,将返回默认值。 /// </summary> TValue1 Value1 { get; set; } /// <summary> /// 设置或获取结果的第二个返回值。若结果不处于成功的状态,将返回默认值。 /// </summary> TValue2 Value2 { get; set; } #endregion Properties #region Methods /// <summary> /// 将当前结果切换到成功的状态,并且清除结果的错误信息。 /// </summary> /// <param name="value1">结果的第一个返回值。</param> /// <param name="value2">结果的第二个返回值。</param> void ToSuccessed(TValue1 value1, TValue2 value2); /// <summary> /// 指示一个结果,与当前结果进行校验。如果 <paramref name="result"/> 是一个错误结果,那么当前的结果将会转换为错误状态。否则将会转换 <paramref name="result"/> 的状态。 /// </summary> /// <param name="result">比较的结果。</param> /// <param name="value1">状态为成功时的第一个返回值。</param> /// <param name="value2">状态为成功时的第二个返回值。</param> /// <returns>返回一个值,表示结果是否为成功状态。</returns> bool Checking(Result result, TValue1 value1, TValue2 value2); #endregion Methods }
Sofire.Data 的所有数据库实现,都派生于 Sofire.Data.QueryEngineBase,通过简单的几个抽象实现,从而达到对数据库的快速支持。
public void DataConnect() { string connectionString = ""; OracleQuery oracleQuery = new OracleQuery(connectionString); // 微软已经不建议使用这种方式连接 Oracle DDTekOracleQuery oleDbQuery = new DDTekOracleQuery(connectionString); MsSqlQuery mssqlQuery = new MsSqlQuery(connectionString); OleDbQuery oleDbQuery = new OleDbQuery(connectionString); SQLiteQuery sqliteQuery = new SQLiteQuery(connectionString); // 派生基类 QueryEngineBase 扩展,可以对更多的数据库提供支持。 }
数据库的查询
public void Execute() { string connectionString = ""; int uid = 1; DDTekOracleQuery oracleQuery = new DDTekOracleQuery(connectionString); TableResult r1 = oracleQuery.ExecuteTable("SELECT * FROM Users WHERE UID=:uid", "uid", uid); if(r1.IsSucceed) { DataTable table = r1.Value; } SqlQuery mssqlQuery = new SqlQuery(connectionString); TableResult r2 = mssqlQuery.ExecuteTable("SELECT * FROM Users WHERE UID=@uid", "@uid", uid); if(r2.IsSucceed) { DataTable table = r2.Value; } }
当然,上面的演示代码仅仅是返回一张表,更多的支持请参考以下图片
由于今年的工作关系,我对 Oracle 的接触频繁,Sofire.Data 中对于 Oracle 的支持,也逐渐成熟中,例如支持多行 ExecuteNoQuery,支持游标参数。
private Result Test1() { //“>”符号表示这是一个存储过程,或程序包 // PKG_FLOW_NAME.UP_GetFlowNameById 的返回值在于一个游标参数。 var tableResult = query.ExecuteTable(">PKG_FLOW_NAME.UP_GetFlowNameById" , new ExecuteParameter("v_FID", 111, DbType.VarNumeric) , query.CreateCursor("c")); if(tableResult.IsSucceed) { Console.WriteLine(tableResult.Value.Rows.Count); } return tableResult; } private Result Test2() { var noQueryResult = query.ExecuteNoQuery(@"begin insert into table1 select * from XXX; insert into table2 select * from XXX; insert into table3 select * from XXX; insert into table4 select * from XXX; end;"); if(tableResult.IsSucceed) { Console.WriteLine(tableResult.Value.Rows.Count); } return tableResult; }
由于时间的关系(最近工作岗位变动),最重要,也是这套框架元老级组件——数据库部分,介绍的并不详细。接下来,可能对 Sofire.Data 的高级部分进行讲解,比如对查询前后的事件支持,查询结果自动转换为对象/集合。不过这种比较代码性的东西,的确比较难阅读,也让我异常纠结。
额,好像很多人想觉得我这款博客皮肤不错?其实这一款博客皮肤是参考 李宝亨达人 的博客风格进行改版的,很感谢他的皮肤(他是绿色版,我是蓝色版)。
我很懒,但如果您在使用这套组件中遇见任何问题或者有任何建议意见,可以在博客留言,我将会及时回复。源码已更新。稍后上传。