c#反射在ORM中的应用(包含特性)
引用:https://www.bilibili.com/video/BV19J411v7yk?p=1
(1)ORM
对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。.NET中EF,Log4就是这种框架。就是通过面向对象的思想达成对数据库的访问。也就是说通过对类的操作达成对数据库的操作。
https://www.cnblogs.com/dfcq/p/12979377.html 面向对象与面向过程区别
什么是“持久化”
持久(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。
什么是 “持久层”
持久层(Persistence Layer),即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。
(2)下面是一个使用反射获取字段属性并赋值的例子
public class People { public People() { Console.WriteLine("{0}被创建",this.GetType().FullName); } public int Id { get; set; } /// <summary> /// 属性Name /// </summary> public string Name { get; set; } /// <summary> /// 字段Description /// </summary> public string Description; }
下面是调用:
static void Main(string[] args) { try { //获取类型--可以不用动态加载,直接获取类型 Type type = typeof(People); //创建对象 object o = Activator.CreateInstance(type); //获取属性并赋值 foreach (PropertyInfo property in type.GetProperties()) { if (property.Name.Equals("Id")) { property.SetValue(o, 12); } else { property.SetValue(o, "测试名称"); } } foreach (FieldInfo filed in type.GetFields()) { if (filed.Name.Equals("Description")) { filed.SetValue(o, "测试描述"); } } //上面是赋值,下面可以获取相应的值 foreach (PropertyInfo property in type.GetProperties()) { Console.WriteLine($"属性名称:{property.Name},值:{property.GetValue(o)}"); } foreach (FieldInfo filed in type.GetFields()) { Console.WriteLine($"字段名称:{filed.Name},值:{filed.GetValue(o)}"); } Console.ReadKey(); } catch (Exception e) { } }
结果
Reflection.Model.People被创建 属性名称:Id,值:12 属性名称:Name,值:测试名称 字段名称:Description,值:测试描述
其实从上面代码来看用反射是很麻烦的,但是如果在增加代码之后用普通代码则需要进行更改代码,但是反射不需要如此。
(3)封装ORM,通过ID做唯一查询
public class SqlHelper { string ConnectionString = @"data source=.;initial catalog=TEST;persist security info=True;user id=sa;password=123;MultipleActiveResultSets=True"; /// <summary> /// 使用反射根据主键查询需要的字段 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="id"></param> /// <returns></returns> public T Query<T>(int id) { Type type = typeof(T); //获取类对象 object obj = Activator.CreateInstance(type); //生成需要查询的字段 string sql = $"select {string.Join(",", type.GetProperties().Select(s => s.Name).ToList())} from {type.Name} where id={id}"; using (SqlConnection sqlConnection = new SqlConnection(ConnectionString)) { sqlConnection.Open(); SqlCommand command = new SqlCommand(sql, sqlConnection); //注意,这里的写法只是局限于只查一条数据 SqlDataReader reader = command.ExecuteReader(); if (reader.Read()) { foreach (var t in type.GetProperties()) { //赋值的时候必须要判断是否为空,因为会报错:Object of type 'System.DBNull' cannot be converted to type 'System.Nullable`1[System.Int32]'.就算是模型里面设置了Nullable或者?,
//只要是查出来的值为空,在赋值的时候还是会报错。 t.SetValue(obj, reader[t.Name] is DBNull ? null : reader[t.Name]); } } } return (T)obj; } public void Update<T>(T model) where T : BaseModel { Type type = model.GetType(); //获取模型中的所有属性值,用于后面列值更改,注意排除主键 string updateString = string.Join(",", type.GetProperties().Where(p => !p.Name.Equals("Id")).Select(p => $"[{p.Name}]=@{p.Name}")); //生成 var paraList = type.GetProperties().Where(p => !p.Name.Equals("Id")).Select(p => new SqlParameter($"@{p.Name}", p.GetValue(model))); string sql = $"update [{type.Name}] set ({updateString}) where {model.Id} "; using (SqlConnection sqlConnection = new SqlConnection(ConnectionString)) { SqlCommand command = new SqlCommand(sql, sqlConnection); command.Parameters.AddRange(paraList.ToArray()); int result = command.ExecuteNonQuery(); if (result != 1) { throw new Exception("失败"); } } } }
调用:
class Program { static void Main(string[] args) { SqlHelper sqlHelper = new SqlHelper(); Company company = sqlHelper.Query<Company>(1); } }
(4)上面自定义的Company的模型中的属性和类型和数据库中表一摸一样,所以可以直接使用上述方法,通过反射完全能够实现这个查询功能,但是如果模型和表字段对应不起来呢?
比如一个表在数据库是Company,但是可能其他原因比如规范之类的限制,程序中必须是CompanyModel,加个后缀,这样的话就导致和数据库表对应不起来,我们可以基于特性来完成映射。看下面代码之前需要对特性有简单的了解。
代码改为:注意看下面红色部分,是做过更改的,也是需要特别注意的
表模型
namespace MyReflection { /// <summary> /// Company:数据表的真实名称 /// </summary> [TableMappingAttribute("Company")] public class CompanyModel { public string CreateTime { get; set; } public string Name { get; set; } public Nullable<int> CreatorId { get; set; } public Nullable<int> LastModifierId { get; set; } public DateTime? LastModifyTime { get; set; } } }
数据查询类:
public class SqlHelper { string ConnectionString = @"data source=.;initial catalog=TEST;persist security info=True;user id=sa;password=123;MultipleActiveResultSets=True"; /// <summary> /// 使用反射根据主键查询需要的字段 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="id"></param> /// <returns></returns> public T Query<T>(int id) { Type type = typeof(T); //获取类对象 object obj = Activator.CreateInstance(type); //这里和上面代码不一样,封装了一个特性扩展类,这样不管程序中的表名和数据库中的表名到底是不是一样的,只要在类型前加上了自定义的TableMappingAttribute特性,
在这里直接调用GetTableName()方法就能得到真正的数据库表名称 string tableName = type.GetTableName(); //生成需要查询的字段 string sql = $"select {string.Join(",", type.GetProperties().Select(s => s.Name).ToList())} from { tableName} where id={id}"; using (SqlConnection sqlConnection = new SqlConnection(ConnectionString)) { sqlConnection.Open(); SqlCommand command = new SqlCommand(sql, sqlConnection); //注意,这里的写法只是局限于只查一条数据 SqlDataReader reader = command.ExecuteReader(); if (reader.Read()) { foreach (var t in type.GetProperties()) { //赋值的时候必须要判断是否为空,因为会报错:Object of type 'System.DBNull' cannot be converted to type 'System.Nullable`1[System.Int32]'.
就算是模型里面设置了Nullable或者?,只要是查出来的值为空,在赋值的时候还是会报错。 t.SetValue(obj, reader[t.Name] is DBNull ? null : reader[t.Name]); } } } return (T)obj; } public void Update<T>(T model) where T : BaseModel { Type type = model.GetType(); //获取模型中的所有属性值,用于后面列值更改,注意排除主键 string updateString = string.Join(",", type.GetProperties().Where(p => !p.Name.Equals("Id")).Select(p => $"[{p.Name}]=@{p.Name}")); //生成 var paraList = type.GetProperties().Where(p => !p.Name.Equals("Id")).Select(p => new SqlParameter($"@{p.Name}", p.GetValue(model))); string sql = $"update [{type.Name}] set ({updateString}) where {model.Id} "; using (SqlConnection sqlConnection = new SqlConnection(ConnectionString)) { SqlCommand command = new SqlCommand(sql, sqlConnection); command.Parameters.AddRange(paraList.ToArray()); int result = command.ExecuteNonQuery(); if (result != 1) { throw new Exception("失败"); } } } }
自定义的表名映射特性:
namespace MyReflection { /// <summary> /// 自定义表名称映射特性 /// </summary> public class TableMappingAttribute : Attribute { private string _MappingName = null; public TableMappingAttribute(string name) { this._MappingName = name; } /// <summary> /// 获取表名称 /// </summary> /// <returns></returns> public string GetMappingName() { return this._MappingName; } } }
还有自定义的特性映射的扩展类,下面的方法通过反射获取到了特性类中的GetMappingName方法,通过这个方法就得到了真实的表名称。
namespace MyReflection { /// <summary> /// 特性映射的扩展 /// </summary> public static class MappingAttributeExtend { /// <summary> /// 注意参数加了this修饰,所以调用方式可以变成下面: /// type.GetTableName(); /// </summary> /// <param name="type"></param> /// <returns></returns> public static string GetTableName(this Type type) { if (type.IsDefined(typeof(TableMappingAttribute), true)) { TableMappingAttribute sd = (TableMappingAttribute)type.GetCustomAttributes(typeof(TableMappingAttribute), true)[0]; return sd.GetMappingName(); } return type.Name; } } }
(5)还有一种情况,那就是程序的模型类中的属性和数据库表中的字段对应不起来,怎么解决?
通过上面我们可以知道,也可以通过自定义特性来解决这个问题,
/// <summary> /// 自定义表属性映射特性 /// </summary> public class ColumnMappingAttribute : Attribute { private string _MappingName = null; public ColumnMappingAttribute(string name) { this._MappingName = name; } /// <summary> /// 获取字段/属性名称 /// </summary> /// <returns></returns> public string GetMappingName() { return this._MappingName; } }
扩展特性类中的获取属性名的方法:
/// <summary> /// 获取表字段名 /// </summary> /// <param name="type"></param> /// <returns></returns> public static string GetColumnName(this PropertyInfo type) { if (type.IsDefined(typeof(ColumnMappingAttribute), true)) { ColumnMappingAttribute sd = (ColumnMappingAttribute)type.GetCustomAttributes(typeof(ColumnMappingAttribute), true)[0]; return sd.GetMappingName(); } return type.Name; }
查询数据方法:红色部分为更改部分
public T Query<T>(int id) { Type type = typeof(T); //获取类对象 object obj = Activator.CreateInstance(type); //这里和上面代码不一样,封装了一个特性扩展类,这样不管程序中的表名和数据库中的表名到底是不是一样的,只要在类型前加上了自定义的TableMappingAttribute特性,
在这里直接调用GetTableName()方法就能得到真正的数据库表名称 string tableName = type.GetTableName(); //生成需要查询的字段 string sql = $"select {string.Join(",", type.GetProperties().Select(s => $"[{s.GetColumnName()}]").ToList())} from { tableName} where id={id}"; using (SqlConnection sqlConnection = new SqlConnection(ConnectionString)) { sqlConnection.Open(); SqlCommand command = new SqlCommand(sql, sqlConnection); //注意,这里的写法只是局限于只查一条数据 SqlDataReader reader = command.ExecuteReader(); if (reader.Read()) { foreach (var t in type.GetProperties()) { //赋值的时候必须要判断是否为空,因为会报错:Object of type 'System.DBNull' cannot be converted to type 'System.Nullable`1[System.Int32]'.
就算是模型里面设置了Nullable或者?,只要是查出来的值为空,在赋值的时候还是会报错。 t.SetValue(obj, reader[t.GetColumnName()] is DBNull ? null : reader[t.GetColumnName()]); } } } return (T)obj; }
但是一个模型可能有很多属性,我们总不能一个一个的加上这些属性特性吧,这样太麻烦了,而且这2个自定义特性一样的地方很多,存在代码复用的情况,所以可以抽象出一个基类来继承。
自定义特性基类:
namespace MyReflection { public class AbstractMappingAttribute:Attribute { private string _MappingName = null; public AbstractMappingAttribute(string name) { this._MappingName = name; } /// <summary> /// 获取真实名称 /// </summary> /// <returns></returns> public string GetMappingName() { return this._MappingName; } } }
自定义的类和属性特性:
namespace MyReflection { /// <summary> /// 自定义表名称映射特性 /// </summary> [AttributeUsage(AttributeTargets.Class)]//表明这个特性只能用于类上 public class TableMappingAttribute : AbstractMappingAttribute { /// <summary> /// 这个写法相当于直接调用父类的构造函数 /// </summary> /// <param name="name"></param> public TableMappingAttribute(string name) : base(name) { } } /// <summary> /// 自定义表属性映射特性 /// </summary> //表明这个特性只能用于属性上,最好加上这个限制 [AttributeUsage(AttributeTargets.Property)] public class ColumnMappingAttribute : AbstractMappingAttribute { /// <summary> /// 这个写法相当于直接调用父类的构造函数 /// </summary> /// <param name="name"></param> public ColumnMappingAttribute(string name) : base(name) { } } }
扩展特性类中的获取名称的方法:
/// <summary> /// 特性映射的扩展 /// </summary> public static class MappingAttributeExtend { /// <summary> /// 之所T限制MemberInfo,是因为不是任何传入 的一个类都具有IsDefined方法的,Type和PropertyInfo类都继承于MemberInfo, /// 所以在这里直接限制只能MemberInfo及其子类能使用 /// AbstractMappingAttribute:是那2个自定义特性类的基类 /// </summary> 这个方法是 /// <param name="type"></param> /// <returns></returns> public static string GetMappingName<T>(this T type) where T : MemberInfo { if (type.IsDefined(typeof(AbstractMappingAttribute), true)) { AbstractMappingAttribute sd = (AbstractMappingAttribute)type.GetCustomAttributes(typeof(TableMappingAttribute), true)[0]; return sd.GetMappingName(); } return type.Name; } /// <summary> /// 注意参数加了this修饰,所以调用方式可以变成下面: /// type.GetTableName(); /// </summary> /// <param name="type"></param> /// <returns></returns> public static string GetTableName(this Type type) { if (type.IsDefined(typeof(TableMappingAttribute), true)) { TableMappingAttribute sd = (TableMappingAttribute)type.GetCustomAttributes(typeof(TableMappingAttribute), true)[0]; return sd.GetMappingName(); } return type.Name; } /// <summary> /// 获取表字段名 /// </summary> /// <param name="type"></param> /// <returns></returns> public static string GetColumnName(this PropertyInfo type) { if (type.IsDefined(typeof(ColumnMappingAttribute), true)) { ColumnMappingAttribute sd = (ColumnMappingAttribute)type.GetCustomAttributes(typeof(ColumnMappingAttribute), true)[0]; return sd.GetMappingName(); } return type.Name; } }
数据查询类:
/// <summary> /// 使用反射根据主键查询需要的字段 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="id"></param> /// <returns></returns> public T Query<T>(int id) { Type type = typeof(T); //获取类对象 object obj = Activator.CreateInstance(type); //这里和上面代码不一样,封装了一个特性扩展类,这样不管程序中的表名和数据库中的表名到底是不是一样的,只要在类型前加上了自定义的TableMappingAttribute特性,
在这里直接调用GetTableName()方法就能得到真正的数据库表名称 string tableName = type.GetMappingName(); //生成需要查询的字段 string sql = $"select {string.Join(",", type.GetProperties().Select(s => $"[{s.GetMappingName()}]").ToList())} from { tableName} where id={id}"; using (SqlConnection sqlConnection = new SqlConnection(ConnectionString)) { sqlConnection.Open(); SqlCommand command = new SqlCommand(sql, sqlConnection); //注意,这里的写法只是局限于只查一条数据 SqlDataReader reader = command.ExecuteReader(); if (reader.Read()) { foreach (var t in type.GetProperties()) { //赋值的时候必须要判断是否为空,因为会报错:Object of type 'System.DBNull' cannot be converted to type 'System.Nullable`1[System.Int32]'.
就算是模型里面设置了Nullable或者?,只要是查出来的值为空,在赋值的时候还是会报错。 t.SetValue(obj, reader[t.GetMappingName()] is DBNull ? null : reader[t.GetMappingName()]); } } } return (T)obj; }
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术