您的位置:首页 > 编程语言 > C#

C#实现简单ORM

2017-08-09 20:08 197 查看

闲话

当天在布置这个任务的时候,一听,实现增删改查,我还天真的想:作业这么简单的么?可是到了实际开始操作的时候才恍然大悟,只恨自己太年轻啊。对于ORM,我的认识还停留在学习java的室友使用的Hibernate上,曾经有一份Entity Framework的教程放在我的面前,我没有珍惜,如果给我一次重新选择的机会,我会好好学习,如果非要给这个学习加上一个权限,我希望是一万年。开个玩笑,虽然对相关知识的不熟悉导致初期很吃力,但终归是完成了,现在缝缝补补,终于看起来像点样子了,这里就对这次ORM作业的完成过程做一个总结,回顾在这个过程中遇到的问题以及解决办法,也趁此机会梳理一下思路。

拿到任务代码的框架,一打开,迎面而来的便是绝望的气息。摆在我面前的是那四个空空如也待实现的方法,而自己完全不知道从哪里下手,于是乎,打开示例代码,网上了解ORM,照着葫芦画瓢,开始code。通过简单的了解,整理出了几点实现这个任务需要的知识,对象关系映射,怎么个映射法?定义的实体类如何和数据表联系起来?反射和Attribute怎样实现前面的问题?这些都需要一一的去解决。

反射和Attribute

突然一来就开始讲反射和Attribute,可能觉得有点突兀,那为什么要在一开始就提到这两个东西呢?由于实现的是简单ORM,所以采用的是一个类映射到一个表的模式,实体类中的属性(Property)和数据库表里的字段一一对应。为了到达这样效果,我们就需要采用反射和Attribute相互合作的模式。Lewis.X.Liu (g-mis.cncd02.Newegg) 42241 > 2017/07/21 > C#实现简单ORM个人心得 > 表关系.jpg

Attribute:MSDN的定义为:公共语言运行时允许添加类似关键字的描述声明,叫做attributes, 它对程序中的元素进行标注,如类型、字段、方法和属性等。Attributes和Microsoft .NET Framework文件的元数据(metadata)保存在一起,可以用来向运行时描述你的代码,或者在程序运行的时候影响应用程序的行为。所以Attribute就是用来为元素添加附加信息的,可以让我们定义具体的映射规则,而通过反射我们又可以动态获取对象信息的能力来获得这些规则。从而实现了实体类到具体的数据库表的映射。特别指出,Attribute和Property中文都为属性,但是Property是实体类中的属性,而Attribute是用于提供附加信息,为坐区分,叫作特性。在本次代码中,定义了三个Attribute,通过他们来描述部分数据库信息。

//表特性
[AttributeUsage(AttributeTargets.Class,
AllowMultiple = false,
Inherited = true)]
public class TableAttribute : System.Attribute
{
public string tableName { get; set; }
public TableAttribute(string name)
{
tableName = name;
}
}

//列特性
[AttributeUsage(AttributeTargets.Property,
AllowMultiple = false,
Inherited = true)]
public class ColumnAttribute : System.Attribute
{
public bool IsPrimaryKey { get; set; }
public string  Name { get; set; }
public Type DataType { get; set; }
public ColumnAttribute(string name)
{
Name = name;
}
}

//主键特性
[AttributeUsage(AttributeTargets.Class)]
public class PrimaryKeyAttribute : System.Attribute
{
public string ID { get; set; }

public PrimaryKeyAttribute(string name)
{
ID = name;
}
}


同时,利用Attribute为实体类添加与数据库字段的对应关系:

[Table("dbo.LewisTable")]
public class User
{
[Column("ID",DataType =typeof(int),IsPrimaryKey =true)]
public int ID { get; set; }
[Column("Name")]
public string Name { get; set; }
[Column("InDate")]
public DateTime InDate { get; set; }
[Column("InUser")]
public string InUser { get; set; }
[Column("LastEditDate")]
public DateTime? LastEditDate { get; set; }
[Column("LastEditUser")]
public string  LastEditUser { get; set; }
}
接下来,就轮到反射上场了,通过反射获得从实体映射出数据库表:
//参数表传入的实体类
TEntity entity = new TEntity();
//获得类型
Type entityType = entity.GetType();
//获取属性
var properties = entityType.GetProperties();
//实体属性和表字段匹配
foreach(var propertyInfo in properties)
{
var attribute = (ColumnAttribute)propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), true)[0];
Console.Write("{0,15}",attribute.Name+":\t\t");
Console.Write(reader[propertyInfo.Name]+"\n");
}


生成SQL语句

在完成了上述的工作以后,我们便要开始考虑操作数据库的问题,这个时候要考虑这几个方面:

如何获得要操作的表名

如何获得主键

如何获得要操作的字段

如何获得字段的值

where子句的条件如何获得

如何获得完整的SQL语句

可空字段如何处理

获取表名

在上面提到的Attribute特性表中,包含了一个TableAttribute,通过这个特性表,我们便可以获得数据库表名称

//获取表名
object[] tableName = entityType.GetCustomAttributes(typeof(TableAttribute), false);
if (tableName != null)
string name = (TableAttribute)tableName[0])).tableName ;


获得主键

当我们进行数据更新的时候,如果有要求不该能更新主键,这时候我们就需要将主键找出来,不把他拼接进入SQL语句中。具体做法和获取表名类似,我们添加了一个主键的特性类,通过IsPrimaryKey就能判断一个字段是否为主键。同时,如果更新语句使用主键ID作为where子句的条件,在进行判断的时候,我们还可以获取主键的值备用,之后可以加入参数列表。

获得字段名称以及值

字段是sql语句中很大的一个部分,他是操作的具体内容。我们通过反射获得字段名称,利用了GetProperties()和GetCustomAttributes()方法。获取了字段名称后,如果是要修改数据库的情况,我们还需要获得实体传入的字段值,这个值通过GetValue()方法获得,与之对应还有SetValue()方法,将在之后空值处理时提到。

PropertyInfo[] proinfos = entityType.GetProperties();
foreach (PropertyInfo property in propertyInfos)
{
object[] myobj = property.GetCustomAttributes(typeof(ColumnAttribute), false);
if (myobj != null)
{
//获取字段名称
string name = ((ColumnAttribute)myobj[0]).Name;
//可空值的处理
if(entityType.GetProperty(name).GetValue(entity,null)!=null)
{
sqlBegin.Append(name + ",");
//获取值
sqlEnd.Append("'" + entityType.GetProperty(name).GetValue(entity, null) + "',");
}
}
}


可空字段的处理

在本次的任务的数据库表中包含了两个可空字段,类型分别为string和DateTime。string类型是引用类型,所以可以为空值,但是DateTime是一个值类型,那么问题便来了,怎么满足将这个字段设置为空的需求呢?最后,寻得DataTime?的解决办法,它为DataTime添加了null值,从而达到了我们的要求。在具体的业务逻辑中,如果实体传入的内容中可空字段并未初始化,也就是值为空,那么便不将这个字段拼接进入SQL中。也可以在参数列表中将其复制为空,这样不会存在字段和参数不匹配的问题,也将空值写入了表中。

//可空值的处理,
if(entityType.GetProperty(name).GetValue(entity,null)!=null)
{
sqlBegin.Append(name + ",");
qlEnd.Append("'" + entityType.GetProperty(name).GetValue(entity, null) + "',");
}

//使用参数列表的情况
else if(LaDate==true&&LaUser==true)
{
rowCount = SqlHelper.Instance.ExecuteNonQuery(conn, upSql,
new List<SqlParameter>() {
new SqlParameter( "@ID",SqlDbType.Int){Value=IDValue },
new SqlParameter( "@Name",SqlDbType.NVarChar,50){ Value=entityType.GetProperty("Name").GetValue(entity, null)},
new SqlParameter( "@InDate",SqlDbType.DateTime){ Value=entityType.GetProperty("InDate").GetValue(entity, null)},
new SqlParameter( "@InUser",SqlDbType.VarChar,15){ Value=entityType.GetProperty("InUser").GetValue(entity, null)},
new SqlParameter( "@LastEditDate",SqlDbType.DateTime){ Value=DBNull.Value},
ew SqlParameter( "@LastEditUser",SqlDbType.VarChar,15){ Value=DBNull.Value},
});


SQL语句拼接

在获得了SQL语句的各组成部分后,就需要将各部分内容拼接起来,构成完整的SQL语句。这里就要提到string和stringBuilder了。string类型有一个不可变特性,所以string对象是之读的,这时候有人会说了,我明明可以改变它的值啊。这个表面上看起来值的改变,实际上是一个新对象的建立,而原有的string对象并未改变。所以,在我们拼接SQL语句的使用场景下,我选择使用StingBuilder。因为每次创建字符串时候,string会先在池中找,如果有相同的,则会拿来用,如果没找到,则会创建一个新的字符串对象,因此在做字符串拼接的时候,特别是循环拼接,实际上在不断的在字符串池中创建字符串对象,因此这样效率就很慢了。

StringBuilder sqlBegin = new StringBuilder();
sqlBegin.Append("");       //拼接Sql语句
StringBuilder sqlEnd = new StringBuilder();
sqlEnd.Append("VALUES(");

if(entityType.GetProperty(name).GetValue(entity,null)!=null)
{
sqlBegin.Append(name + ",");
sqlEnd.Append("'" + entityType.GetProperty(name).GetValue(entity, null) + "',");
}
sqlBegin.Append(")");
sqlBegin.Replace(",)", ")");
//修正尾部多余的符号
sqlEnd.Append(")");
sqlEnd.Replace(",)", ") SELECT SCOPE_IDENTITY()");
string sql = "INSERT INTO " + sqlBegin.ToString() + sqlEnd.ToString();


至此,任务就基本完成了,传入实体对象,调用即可。

结语

思路凌乱,希望各位大佬批评指正,感激不尽。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: