编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

C#.Net如何手写ORM

wxchong 2024-06-28 10:29:28 开源技术 12 ℃ 0 评论

快速认识ORM

对象-关系映射,即Object/Relation Mapping,主要实现程序对象到关系数据库的映射。现在.Net比较流行的ORM框架有:EF、SqlSugar、Dapper、FreeSql、Nhibernate、IBatis.Net等。

O/RM只是一层代码的封装,底层还是基于ADO.NET完成对数据库的访问。

一般写法

如果我们要写一个查询,用ADO.NET就会如下这样写。

private static string ConnectionStringCustomers = "Data Source=.;Database=Customers;" +    "User ID=sa;Password=123456;MultipleActiveResultSets=True"; public Company FindCompany(int id){    string sql = $@"        SELECT [Id],[Name],[CreateTime],[CreatorId],               [LastModifierId],[LastModifyTime]        FROM [dbo].[Company]         WHERE ID = {id}";    using (SqlConnection conn = new SqlConnection(ConnectionStringCustomers))    {        SqlCommand command = new SqlCommand(sql, conn);        conn.Open();        var reader = command.ExecuteReader();        if (reader.Read())        {            Company company = new Company()            {                Id = (int)reader["Id"],                Name = reader["Name"].ToString()            };            return company;        }        else        {            return null;        }    }}
public abstract class BaseModel{    public int Id { set; get; }}   public class Company : BaseModel{    public string Name { get; set; }     public DateTime CreateTime { get; set; }     public int CreatorId { get; set; }     public int? LastModifierId { get; set; }     public DateTime? LastModifyTime { get; set; }}

但这样的写法是写死了的,我们能不能写一个通用查询,不管他是哪张表。

通用查询

既然要通用,那就不能写死类型,我们想到了使用泛型。泛型是任何类型都可以用,为了保证类型正确,我们再加泛型约束。
为了得到属性,我们要使用反射获取。

public T Find<T>(int id) where T : BaseModel // 泛型约束,必须继承自BaseModel{    string colums = string.Join(",", typeof(T).GetProperties().Select(p => #34;[{p.Name}]").ToArray());    string sql = #34;SELECT {colums} FROM [{typeof(T).Name}] WHERE Id={id}";     using (SqlConnection conn = new SqlConnection(ConnectionStringCustomers))    {        SqlCommand command = new SqlCommand(sql, conn);        conn.Open();        var reader = command.ExecuteReader();        if (reader.Read())        {            // 反射实例化            T t = Activator.CreateInstance<T>();            foreach (var property in typeof(T).GetProperties())            {                property.SetValue(t, reader[property.Name] is DBNull ? null : reader[property.Name]);            }            return t;        }        else        {            return null;        }    }    return default(T);}

上述的方法,使用泛型和反射使我们的查询可以通用。
然后使用
Company Company = sqlHelper.Find<Company>(1);来调用得到实体。

但是,我们还有一个问题,如果我们的表名和实体类名称不一样,或字段名不一样,比如:表名为Sys_Company而实体名为Company,那我们该如何映射?
这里我们打算用C#的特性来解决这一问题。

首先,创建用来映射的特性类。

public class AbstractMappingAttribute : Attribute{    public string MappingName = null;    public AbstractMappingAttribute(string mappingName)    {        this.MappingName = mappingName;    }}

映射表名。

[AttributeUsage(AttributeTargets.Class)]public class DBProxyTableAttribute: AbstractMappingAttribute{    public DBProxyTableAttribute(string tableName) : base(tableName){}}

映射列名。

    [AttributeUsage(AttributeTargets.Property)]    public class DBProxyColumnAttribute : AbstractMappingAttribute    {        public DBProxyColumnAttribute(string columnName):base(columnName) {}    }

在类名上添加特性。

[DBProxyTable("Sys_Company")]public class Company : BaseModel{    [DBProxyColumn("Company_Name")]    public string Name { get; set; }    ......}

获取实体类名或属性上的特性值来映射数据库的方法。

public static class DBProxyMappingExtension{    public static string GetMappingName(this MemberInfo member)    {        string name = null;        if (member.IsDefined(typeof(AbstractMappingAttribute), true))        {            var attribute = member.GetCustomAttribute<AbstractMappingAttribute>();            name = attribute.MappingName;        }        else        {            name = member.Name;        }        return name;    }}

最后,重新修改通用方法。

public T Find<T>(int id) where T : BaseModel // 泛型约束,必须继承自BaseModel{    //string colums = string.Join(",", typeof(T).GetProperties().Select(p => #34;[{p.Name}]").ToArray());    string colums = string.Join(",", typeof(T).GetProperties().Select(p => #34;[{p.GetMappingName()}]").ToArray());     //string sql = #34;SELECT {colums} FROM [{typeof(T).Name}] WHERE Id={id}";    string sql = #34;SELECT {colums} FROM [{typeof(T).GetMappingName()}] WHERE Id={id}";     using (SqlConnection conn = new SqlConnection(ConnectionStringCustomers))    {        SqlCommand command = new SqlCommand(sql, conn);        conn.Open();        var reader = command.ExecuteReader();        if (reader.Read())        {            // 反射实例化            T t = Activator.CreateInstance<T>();            foreach (var property in typeof(T).GetProperties())            {                //property.SetValue(t, reader[property.Name] is DBNull ? null : reader[property.Name]);                property.SetValue(t, reader[property.GetMappingName()] is DBNull ? null : reader[property.GetMappingName()]);            }            return t;        }        else        {            return null;        }    }    return default(T);}

通用插入

我们先写一个泛型缓存类:

public class SQLCacheBuilder<T> where T : BaseModel{    private static string InsertSQL = "";    /// <summary>    /// 静态构造函数,由CLR保障,在第一次使用该类之前,完成调用且只调用一次    /// </summary>    static SQLCacheBuilder()    {        Console.WriteLine("SQLCacheBuilder 静态ctor。。。。。。。");        string columns = string.Join(",", typeof(T).GetPropertiesNoKey()            .Select(p => #34;[{p.GetMappingName()}]"));        string values = string.Join(",", typeof(T).GetPropertiesNoKey()            .Select(p => #34;@{p.GetMappingName()}"));            InsertSQL = #34;INSERT INTO [{typeof(T).GetMappingName()}] " +                #34;({columns}) VALUES ({values})";    }     public static string GetInsertSQL()    {        return InsertSQL;    }}

当第一次调用SQLCacheBuilder方法对InsertSQL赋值,那么再次调用就会直接取缓存中的InsertSQL。但如果调用SQLCacheBuilder<T>类,传来的泛型T不同,则缓存的InsertSQL是不同的。InsertSQL就是我们要执行的sql语句。

我们数据库表设置的id是自增的,为了在插入的SQL中过滤掉Id字段,我们打算用特性过滤。

[AttributeUsage(AttributeTargets.Property)]public class DBProxyKeyAttribute: Attribute{} public static class DBProxyFilterAttributeExtension{    public static IEnumerable<PropertyInfo> GetPropertiesNoKey(this Type type)    {        return type.GetProperties()            .Where(p => !p.IsDefined(typeof(DBProxyKeyAttribute), true));    }}

然后在实体属性id上加上此特性:

     [DBProxyKey]     public int Id { set; get; }

这样只要调用了GetPropertiesNoKey方法,遇见属性上加有DBProxyKey特性则会过滤掉。

最后,就是我们的通用的插入数据的方法:

// 写一个通用插入方法public bool Insert<T>(T t) where T : BaseModel{    // 调用SQLCacheBuilder方法获得拼接的sql    string sql = SQLCacheBuilder<T>.GetInsertSQL();    // 为了防止拼接的有sql注入,使用参数parameters    var parameters = typeof(T).GetPropertiesNoKey()        .Select(p => new SqlParameter(#34;@{p.GetMappingName()}",         p.GetValue(t) is null ? DBNull.Value : p.GetValue(t)));           return this.ExecuteSQL<bool>(sql, parameters.ToArray(), command =>    {        int iResult = command.ExecuteNonQuery();        return iResult == 1;    });} private T ExecuteSQL<T>(string sql, SqlParameter[] parameters, Func<SqlCommand, T> func){    using (SqlConnection conn = new SqlConnection(ConnectionStringCustomers))    {        SqlCommand command = new SqlCommand(sql, conn);        command.Parameters.AddRange(parameters);        conn.Open();        return func.Invoke(command);    }}

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表