“造轮运动”之 ORM框架系列(三)~ 干货呈上
这一趴里面,我就来正式介绍一下CoffeeSQL的干货。
首先要给CoffeeSQL来个定位:最开始就是由于本人想要了解ORM框架内部的原理,所以就四处搜寻有关的博客与学习资料,就是在那个夏天,在博客园上看到了一位7tiny老哥的博客(https://www.cnblogs.com/7tiny/p/9575230.html),里面基本上包含了我所想要了解的全套内容。幸得7tiny老哥的博客和代码都写的非常清晰,所以没花多久时间就看完了源码并洞悉其中奥妙,于是自己就有个想法:在7tiny的开源代码的基础上归纳自己的ORM框架。于是出于学习与自我使用的目的就开始了扩展功能的道路,到现在为止,自己已经在公司的一个项目中用上了,效果还不错。在这里也感谢7tiny老哥对我提出的一些问题及时的回复和指导,真心感谢。
一、框架模块介绍
根据CoffeeSQL的功能模块组成来划分,可以分为:数据库连接管理、SQL命令执行入口、SQL命令生成器、SQL查询引擎、ORM缓存机制、实体数据验证 这六个部分,CoffeeSQL的操作入口与其他的ORM框架一样,都是以数据库上下文(DBContext)的方式进行操作。整体结构图如下:
下面就大致地介绍一下每一个模块的具体功能与实现的思路:
1、数据库连接管理(DBConnectionManagement)
数据库连接的管理实际上就是对数据库连接字符串与其对应的数据库连接对象的管理机制,它可以保证在进行一主多从的数据库部署时ORM帮助我们自动地切换连接的数据库,而且还支持 <最小使用>与 <轮询>两种数据库连接切换策略。
2、SQL命令执行入口(QueryExecute)
QueryExecute是CoffeeSQL生成的所有sql语句执行的入口,执行sql语句并返回结果,贯穿整个CoffeeSQL最核心的功能就是映射sql查询结果到实体,这里采用的是构建表达式树的技术,性能大大优于反射获取实体的方式,具体的两者速度对比的实验在7tiny的博客中有详细介绍,大家可以移步观看(https://www.cnblogs.com/7tiny/p/9861166.html),在我的博客(https://www.cnblogs.com/MaMaNongNong/p/12173620.html)中我使用表达式树的技术造了个简练版的OOM框架。
这里贴出核心代码,方便查看:
1 /// <summary> 2 /// Auto Fill Adapter 3 /// => Fill DataRow to Entity 4 /// </summary> 5 public class EntityFillAdapter<Entity> 6 { 7 private static readonly Func<DataRow, Entity> funcCache = GetFactory(); 8 9 public static Entity AutoFill(DataRow row) 10 { 11 return funcCache(row); 12 } 13 14 private static Func<DataRow, Entity> GetFactory() 15 { 16 #region get Info through Reflection 17 var entityType = typeof(Entity); 18 var rowType = typeof(DataRow); 19 var convertType = typeof(Convert); 20 var typeType = typeof(Type); 21 var columnCollectionType = typeof(DataColumnCollection); 22 var getTypeMethod = typeType.GetMethod("GetType", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(string) }, null); 23 var changeTypeMethod = convertType.GetMethod("ChangeType", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(object), typeof(Type) }, null); 24 var containsMethod = columnCollectionType.GetMethod("Contains"); 25 var rowIndexerGetMethod = rowType.GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, new[] { new ParameterModifier(1) }); 26 var columnCollectionIndexerGetMethod = columnCollectionType.GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(int) }, new[] { new ParameterModifier(1) }); 27 var entityIndexerSetMethod = entityType.GetMethod("set_Item", BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(object) }, null); 28 var properties = entityType.GetProperties(BindingFlags.Instance | BindingFlags.Public); 29 #endregion 30 31 #region some Expression class that can be repeat used 32 //DataRow row 33 var rowDeclare = Expression.Parameter(rowType, "row"); 34 //Student entity 35 var entityDeclare = Expression.Parameter(entityType, "entity"); 36 //Type propertyType 37 var propertyTypeDeclare = Expression.Parameter(typeof(Type), "propertyType"); 38 //new Student() 39 var newEntityExpression = Expression.New(entityType); 40 //row == null 41 var rowEqualnullExpression = Expression.Equal(rowDeclare, Expression.Constant(null)); 42 //row.Table.Columns 43 var rowTableColumns = Expression.Property(Expression.Property(rowDeclare, "Table"), "Columns"); 44 //int loopIndex 45 var loopIndexDeclare = Expression.Parameter(typeof(int), "loopIndex"); 46 //row.Table.Columns[loopIndex].ColumnName 47 var columnNameExpression = Expression.Property(Expression.Call(rowTableColumns, columnCollectionIndexerGetMethod, loopIndexDeclare), "ColumnName"); 48 //break; 49 LabelTarget labelBreak = Expression.Label(); 50 //default(Student) 51 var defaultEntityValue = Expression.Default(entityType); 52 #endregion 53 54 var setRowNotNullBlockExpressions = new List<Expression>(); 55 56 #region entity = new Student();loopIndex = 0; 57 setRowNotNullBlockExpressions.Add(Expression.Assign(entityDeclare, newEntityExpression)); 58 setRowNotNullBlockExpressions.Add(Expression.Assign(loopIndexDeclare, Expression.Constant(0))); 59 60 #endregion 61 62 #region loop Fill DataRow's field to Entity Indexer 63 /* 64 * while (true) 65 * { 66 * if (loopIndex < row.Table.Columns.Count) 67 * { 68 * entity[row.Table.Columns[loopIndex].ColumnName] = row[row.Table.Columns[loopIndex].ColumnName]; 69 * loopIndex++; 70 * } 71 * else break; 72 * } 73 */ 74 75 setRowNotNullBlockExpressions.Add( 76 77 Expression.Loop( 78 Expression.IfThenElse( 79 Expression.LessThan(loopIndexDeclare, Expression.Property(rowTableColumns, "Count")), 80 Expression.Block( 81 Expression.Call(entityDeclare, entityIndexerSetMethod, columnNameExpression, Expression.Call(rowDeclare, rowIndexerGetMethod, columnNameExpression)), 82 Expression.PostIncrementAssign(loopIndexDeclare) 83 ), 84 Expression.Break(labelBreak) 85 ), 86 labelBreak 87 ) 88 ); 89 #endregion 90 91 #region assign for Entity property 92 foreach (var propertyInfo in properties) 93 { 94 var columnAttr = propertyInfo.GetCustomAttribute(typeof(ColumnAttribute), true) as ColumnAttribute; 95 96 // no column , no translation 97 if (null == columnAttr) continue; 98 99 if (propertyInfo.CanWrite) 100 { 101 var columnName = Expression.Constant(columnAttr.GetName(propertyInfo.Name), typeof(string)); 102 103 //entity.Id 104 var propertyExpression = Expression.Property(entityDeclare, propertyInfo); 105 //row["Id"] 106 var value = Expression.Call(rowDeclare, rowIndexerGetMethod, columnName); 107 //default(string) 108 var defaultValue = Expression.Default(propertyInfo.PropertyType); 109 //row.Table.Columns.Contains("Id") 110 var checkIfContainsColumn = Expression.Call(rowTableColumns, containsMethod, columnName); 111 //!row["Id"].Equals(DBNull.Value) 112 var checkDBNull = Expression.NotEqual(value, Expression.Constant(System.DBNull.Value)); 113 114 var propertyTypeName = Expression.Constant(propertyInfo.PropertyType.ToString(), typeof(string)); 115 116 /* 117 * if (row.Table.Columns.Contains("Id") && !row["Id"].Equals(DBNull.Value)) 118 * { 119 * propertyType = Type.GetType("System.String"); 120 * entity.Id = (string)Convert.ChangeType(row["Id"], propertyType); 121 * } 122 * else 123 * entity.Id = default(string); 124 */ 125 setRowNotNullBlockExpressions.Add( 126 127 Expression.IfThenElse( 128 Expression.AndAlso(checkIfContainsColumn, checkDBNull), 129 Expression.Block( 130 Expression.Assign(propertyTypeDeclare, Expression.Call(getTypeMethod, propertyTypeName)), 131 Expression.Assign(propertyExpression, Expression.Convert(Expression.Call(changeTypeMethod, value, propertyTypeDeclare), propertyInfo.PropertyType)) 132 ), 133 Expression.Assign(propertyExpression, defaultValue) 134 ) 135 ); 136 } 137 } 138 139 #endregion 140 141 var checkIfRowIsNull = Expression.IfThenElse( 142 rowEqualnullExpression, 143 Expression.Assign(entityDeclare, defaultEntityValue), 144 Expression.Block(setRowNotNullBlockExpressions) 145 ); 146 147 var body = Expression.Block( 148 149 new[] { entityDeclare, loopIndexDeclare, propertyTypeDeclare }, 150 checkIfRowIsNull, 151 entityDeclare //return Student; 152 ); 153 154 return Expression.Lambda<Func<DataRow, Entity>>(body, rowDeclare).Compile(); 155 } 156 } 157 158 #region 159 //public class Student : EntityDesign.EntityBase 160 //{ 161 // [Column] 162 // public string Id { get; set; } 163 164 // [Column("StudentName")] 165 // public string Name { get; set; } 166 //} 167 ////this is the template of "GetFactory()" created. 168 //public static Student StudentFillAdapter(DataRow row) 169 //{ 170 // Student entity; 171 // int loopIndex; 172 // Type propertyType; 173 174 // if (row == null) 175 // entity = default(Student); 176 // else 177 // { 178 // entity = new Student(); 179 // loopIndex = 0; 180 181 // while (true) 182 // { 183 // if (loopIndex < row.Table.Columns.Count) 184 // { 185 // entity[row.Table.Columns[loopIndex].ColumnName] = row[row.Table.Columns[loopIndex].ColumnName]; 186 // loopIndex++; 187 // } 188 // else break; 189 // } 190 191 // if (row.Table.Columns.Contains("Id") && !row["Id"].Equals(DBNull.Value)) 192 // { 193 // propertyType = Type.GetType("System.String"); 194 // entity.Id = (string)Convert.ChangeType(row["Id"], propertyType); 195 // } 196 // else 197 // entity.Id = default(string); 198 199 // if (row.Table.Columns.Contains("StudentName") && !row["StudentName"].Equals(DBNull.Value)) 200 // { 201 // propertyType = Type.GetType("System.String"); 202 // entity.Name = (string)Convert.ChangeType(row["StudentName"], propertyType); 203 // } 204 // else 205 // entity.Name = default(string); 206 // } 207 208 // return entity; 209 //} 210 #endregion
3、SQL查询引擎(QueryEngine)
SQL查询引擎的功能主要就是以函数的形式来构建查询SQL的结构。将sql语句使用高级语言的函数来进行构建能大大减轻程序员必须一丝不苟编写sql语句的压力。特别是在使用强类型查询引擎时以Lambda表达式的方式编写程序,相当舒适的体验;对于稍微复杂的sql,建议使用弱类型查询引擎来构建sql查询语句,同时也提供方便的分页功能,用法与Dapper类似;再复杂一点的数据库查询逻辑可能你就要考虑使用存储过程查询引擎了,总之,有了这三个查询引擎,所有的查询需求都能满足了。最后一个是update的执行引擎,它被用来构建update的语句。
4、实体数据验证(EntityValidation)
实体数据验证是完全独立的一部分,主要用来检验实体类中字段值的合法性,相当于在高级语言层面对即将持久化到数据库表中的数据进行预先的字段合法性校验,避免在持久化过程中发生不必要的字段格式不合法的错误。
5、ORM缓存机制(ORMCache)
这里的ORM缓存主要分为两级缓存,一级缓存为以sql语句为缓存键的缓存,缓存的内容就是当前执行的sql语句的执行结果;而二级缓存则是以表名为缓存键的表缓存,就是会把一整个表的数据全部存入缓存中,所以表缓存最适合那些数据量不大且查询频繁的表。
6、SQL命令生成器【强类型】(CommandTextGenerator)
在使用诸如强类型查询引擎、Update执行引擎等进行了强类型的SQL语句构造后,相应的sql构造信息都要通过SQL命令生成器来生成最终可由数据库执行的sql语句。SQL命令生成器扮演的就是类似于翻译官的角色,将高级语言中的语句转化为数据库中的sql语句。在实际的应用场景中还可以根据不同的数据库类型将SQL命令生成器扩展成诸如Mysql-SQL命令生成器或者Oracle-SQL命令生成器以符合不同类型数据库的不同sql语法。
7、数据库上下文(DBContext)
作为整个CoffeeSQL的操作入口,DBContext类涵盖了各种配置参数字段与增删改查的API调用函数。其中在事务处理中,由于写操作都是通过对主库的操作,所以在事务处理中是以主库作为事务处理的对象。
二、使用方式
下载CoffeeSql源码进行编译,你会得到 CoffeeSql.Core.dll、CoffeeSql.Oracle.dll、CoffeeSql.Mysql.dll 三个dll文件,其中CoffeeSql.Core.dll为必选,然后根据你的数据库类型选择是CoffeeSql.Oracle.dll或者CoffeeSql.Mysql.dll,目前还只支持这两种数据库,后续会支持更多数据库。
三、展望
路漫漫其修远兮,吾将上下而求索,对比市面上火热的ORM框架,CoffeeSQL还是缺少了一些实用的功能,对这个ORM框架的展望中我会考虑以下一些功能:
1、CodeFirst、DbFirst功能的支持,可以快捷方便地进行实体类与数据库建表sql的生成;
2、批量插入操作的实现,可以提高批量插入数据的性能;
3、对多表联合查询的lambda语法支持;
介绍的再多都不如读一遍源码来的实在,有想深入了解orm原理的小伙伴可以阅读一下源码,真的SO EASY!
源码地址:https://gitee.com/xiaosen123/CoffeeSqlORM
本文为作者原创,转载请注明出处:https://www.cnblogs.com/MaMaNongNong/p/12896787.html