.NET8关于ORM的一次思考
文章目录
- 前言
- 一、思路
- 二、实现ODBC=>SqlHelper.cs
- 三、数据对象实体化
- 四、SQL生成SqlBuilder.cs
- 五、参数注入 SqlParameters.cs
- 六、反射 SqlOrm.cs
- 七、自定义数据查询
- 八、总结
前言
琢磨着在.NET8找一个ORM,对比了最新的框架和性能。
框架 | 批量操作性能 | SQL控制粒度 | 学习成本 | 扩展性 |
---|---|---|---|---|
Dapper | ★★★★☆ | 完全自主 | 低 | 依赖扩展库 |
SqlSugar | ★★★★☆ | 半自动 | 中 | 内置优化 |
EF Core | ★★☆☆☆ | 自动生成 | 高 | 高度可扩展 |
ODBC | ★★☆☆☆ | 完全自主 | 低 | 依赖驱动 |
Dapper在1000条以内和10万以上的数据都是最快的,且粒度小。
毕竟Dapper够轻量:仅26个核心类,无复杂映射配置。
但当我看到它的驱动依旧是System.Data.SqlClient时,我思索,为什么我不自己写一个呢。
毕竟我的逻辑大多在存储过程里。框架也是人家重构出来的嘛,人家的不一定是适合自己的。
尝试着在.net8里重构了一下并记录下来
一、思路
1、泛型实体缩减代码
2、参数化语句,保障性能和安全
3、以dataset为主、model的二次操作为辅,实现数据的快速Josn化,向接口传输
4、事务可选化,因为主要业务的存储过程我可以单独写事务。
5、异步提升效率。
6、使用lamada精简代码
二、实现ODBC=>SqlHelper.cs
Nuget 安装Microsoft.Data.SqlClient包。
主要方法
1、ExecuteNonQuery 返回执行条数,作为编辑用。
2、ExecuteScalar返回执行Object ,作为查询用。
这区分开了就很好实现了,增、改、删我用1、查我用2。思路清晰。
using (SqlConnection connection = new SqlConnection(connectionString)){await connection.OpenAsync();using (SqlCommand cmd = new SqlCommand(sql, connection)){return await cmd.ExecuteNonQueryAsync();//ExecuteScalar()}
}
主要连接就这个了,使用using 实现资源释放。
加上我们提到的重构一下:
using Microsoft.Data.SqlClient;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Transactions;namespace DataProcess
{/// <summary>/// ODBC 辅助类/// </summary>public class SqlHelper{public static string ConnectionString = "";private static int timeout = 30;private static string _empty = "null";/// <summary>/// 返回查询结果/// </summary>/// <param name="SqlStr"></param>/// <returns></returns>public static async Task<object> MyTran(List<CmdData> list, string type, bool IsTran){try{using (SqlConnection connection = new SqlConnection(ConnectionString)){await connection.OpenAsync();if (type == "Edit")// 执行SQL 返回影响条数return await Edit(connection, CommandType.Text, list, IsTran);if (type == "QueryDs")// 执行SQL 返回查询结果DataSetreturn await QueryDs(connection, CommandType.Text, list);if (type == "QueryInt")// 执行SQL 返回查询结果 第一行第一列的int return await QueryInt(connection, CommandType.Text, list);elsereturn _empty;}}catch (SqlException ex){Console.WriteLine($"数据库错误:{ex.Message}");return _empty;}}/// <summary>/// 非查询 =>条数 --存储过程 Istran=0/// </summary>/// <returns></returns>public static async Task<object> Edit(SqlConnection connection, CommandType cmdType, List<CmdData> list, bool IsTran){SqlTransaction trans = null;if (IsTran) //是否开启事务trans = connection.BeginTransaction();try{int result = 0;for (int i = 0; i < list.Count; i++)//批量删除{using (SqlCommand cmd = new SqlCommand(list[i].sql, connection)){if (IsTran) cmd.Transaction = trans;cmd.CommandType = cmdType;if (list[i].parameter != null) cmd.Parameters.AddRange(list[i].parameter);cmd.CommandTimeout = timeout; // 超时定义int rst = await cmd.ExecuteNonQueryAsync();if (rst == 0){ //没执行成功if (IsTran) await trans.RollbackAsync();else break;}else result += rst;}if (IsTran) await (trans).CommitAsync();}return result;}catch (Exception e){if (IsTran) await trans.RollbackAsync();return _empty;}}/// <summary>/// 查询 =>数据 Int/// </summary>public static async Task<object> QueryInt(SqlConnection connection, CommandType cmdType, List<CmdData> list){using (SqlCommand cmd = new SqlCommand(list[0].sql, connection)){cmd.CommandType = cmdType;if (list[0].parameter != null) cmd.Parameters.AddRange(list[0].parameter);cmd.CommandTimeout = timeout; // 超时定义try{var rst = await cmd.ExecuteScalarAsync() ;return rst;}catch (Exception e){return _empty;}}}/// <summary>/// 查询 =>数据 DataSet /// </summary>public static async Task<object> QueryDs(SqlConnection connection, CommandType cmdType, List<CmdData> list){using (SqlCommand cmd = new SqlCommand(list[0].sql, connection)){cmd.CommandType = cmdType;if (list[0].parameter != null) cmd.Parameters.AddRange(list[0].parameter);cmd.CommandTimeout = timeout; // 超时定义try{using (DbDataReader reader = await cmd.ExecuteReaderAsync()){DataSet rst = ConvertDataReaderToDataSet(reader);return rst;}}catch (Exception e){return _empty;}}}public static DataSet ConvertDataReaderToDataSet(DbDataReader reader){DataSet dataSet = new DataSet();// 处理第一个结果集DataTable schemaTable = reader.GetSchemaTable();DataTable dataTable = new DataTable();// 自动构建列结构(根据DataReader的元数据)dataTable.Load(reader); // 此方法自动映射列并填充数据[^2]dataSet.Tables.Add(dataTable);// 处理后续结果集(如果存在)while (!reader.IsClosed && reader.NextResult()){DataTable nextTable = new DataTable();nextTable.Load(reader);dataSet.Tables.Add(nextTable);}return dataSet;}}//存储过程对象public class StoredProc{public string Name { get; set; }public List<ProcParam> param { get; set; }}public class ProcParam {public ProcParam(string Name, object value){this.Name = Name;this.value = value;}public string Name { get; set; }public object value { get; set; }}
webapi 直接给 sqlhelp.connectionString 赋值,就能实现数据连接了。
三、数据对象实体化
要实现泛型,那肯定要先拿到数据实体的model
数据实体手敲那肯定不和谐,现在实现一键转换的工具还是很多的。我使用EF Core Power Tools,
连接数据库,并选中所有表
这样就能自动生成跟数据库字段对应的表对象实体了。
四、SQL生成SqlBuilder.cs
SQL是生成分两部分,主体、参数。增删查改还是有迹可循的。
using Microsoft.Data.SqlClient;
using Model;
using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;namespace DataProcess
{/// <summary>/// T-SQL DIY/// </summary>public class SqlBuilder{#region 编辑Edit/// <summary>/// 实体 泛型 Insert 语句/// </summary>public static string InsertSQL<T>(T entity){Type type = typeof(T);string columns = "";string values = "";foreach (PropertyInfo prop in type.GetProperties()){if (prop.Name == "Id") continue; // 排除自增主键columns+=$"{prop.Name},";values+=$"@{prop.Name},";}return $"INSERT INTO {type.Name} ({columns.TrimEnd(',')}) VALUES ({values.TrimEnd(',')});";}public static string InsertSQL_id<T>(T entity){return InsertSQL(entity) + "Select @@IDENTITY AS LastID;";}public static string UpdateSQL<T>(T entity){Type type = typeof(T);string temp = "";foreach (PropertyInfo prop in type.GetProperties()){if (prop.Name == "Id") { continue; } // 排除自增主键temp += string.Format("{0}=@{0},", prop.Name);}return $"UPDATE {type.Name} SET ({temp.TrimEnd(',')} WHERE @id=id";}public static string DeleteSQL<T>(T entity) {string objectName = entity.GetType().Name;string sql = string.Format("delete from {0} where id=@id;", objectName);return sql;}#endregion#region 查询 querypublic static string SelectSQL<T>(string id) where T: new(){string ModelName = getName<T>();string sql = string.Format("Select * from {0} where id={1};", ModelName, id);return sql;}public static string ByPageSQL<T>(string strWhere, string orderby, int startIndex, int endIndex) where T : new(){string ModelName = getName<T>();StringBuilder strSql = new StringBuilder();strSql.Append("Select * FROM ( ");strSql.Append(" Select ROW_NUMBER() OVER (");if (!string.IsNullOrEmpty(orderby.Trim())){strSql.Append("order by T." + orderby);}else{strSql.Append("order by T.id desc");}strSql.Append(")AS Row, T.* from " + ModelName + " T ");if (!string.IsNullOrEmpty(strWhere.Trim())){strSql.Append(" WHERE " + strWhere);}strSql.Append(" ) TT");strSql.AppendFormat(" WHERE TT.Row between {0} and {1}", startIndex, endIndex);return strSql.ToString();}public static string ListSQL<T>(string strWhere) where T : new(){string ModelName = getName<T>();if (string.IsNullOrEmpty(strWhere)) strWhere = "1=1";string sql = string.Format("Select * from {0} where {1};", ModelName, strWhere);return sql;}public static string CountSQL<T>(string strWhere) where T : new(){string ModelName = getName<T>();if (string.IsNullOrEmpty(strWhere)) strWhere = "1=1";string sql = string.Format("Select count(1) from {0} where {1};", ModelName, strWhere);return sql;}public static string ProcSQL(StoredProc T){string columns = "";if(string.IsNullOrEmpty(T.ActionType))columns += $"@ActionType={T.ActionType},";foreach (ProcParam item in T.param){columns += $"@{item.Name}={item.value},";}return $"EXEC {T.Name} {columns.TrimEnd(',')};";}#endregion/// <summary>/// 实例以取表名/// </summary>public static string getName<T>() where T : new(){T t = new T();string name = t.GetType().Name;return name;}}
}
五、参数注入 SqlParameters.cs
using Microsoft.Data.SqlClient;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading.Tasks;namespace DataProcess
{/// <summary>/// 实体注入/// </summary>public class SqlParameters{/// <summary>/// for add/// </summary>public static SqlParameter[] NoID(object entity){return entity.GetType().GetProperties().Where(p => p.Name != "Id").Select(p => new SqlParameter($"@{p.Name}", p.GetValue(entity) ?? DBNull.Value)).ToArray();}/// <summary>/// for delete/// </summary>public static SqlParameter[] OnlyID(object entity){return entity.GetType().GetProperties().Where(p => p.Name == "Id").Select(p => new SqlParameter($"@{p.Name}", p.GetValue(entity) ?? DBNull.Value)).ToArray();}/// <summary>/// for update/// </summary>public static SqlParameter[] HaveId(object entity){SqlParameter[] rst = NoID(entity);SqlParameter[] rst2 = OnlyID(entity);rst.Append(rst2[0]);return rst;}}
}
根据加、删、改,分别循环实体的value注入进去。当然我的每个表都是自增id作为主键的。
六、反射 SqlOrm.cs
增、删、改的Edit部分完成了,现在实现 查询部分。
通过循环实体属性,将DataSet转换为model,并完成赋值
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace DataProcess
{/// <summary>/// DataRow to Model/// </summary>public class SqlOrm{/// <summary>/// 反射/// </summary>public static T ToEntity<T>(DataRow row) where T : new(){//初始化T t = new T();//得到类型Type type = t.GetType();//属性集合PropertyInfo[] ps = type.GetProperties();//赋值、格式转换ps.ToList().ForEach(p =>{var _data = row[p.Name];//值if (_data != null && _data != DBNull.Value)//非空{if (p.PropertyType == typeof(System.Nullable<System.DateTime>))//空时间p.SetValue(t, DateTime.Parse(_data.ToString() ?? "1970-01-01"), null);else if (p.PropertyType == typeof(System.Nullable<System.Decimal>))//空Decimalp.SetValue(t, Decimal.Parse(_data.ToString() ?? "0"), null);else if (p.PropertyType == typeof(System.Nullable<System.Int32>))//Int32p.SetValue(t, Int32.Parse(_data.ToString() ?? "0"), null);elsep.SetValue(t, Convert.ChangeType(_data, p.PropertyType), null);}elsep.SetValue(t, Convert.ChangeType(_data, p.PropertyType), null);});return t;}}
}
七、自定义数据查询
using Microsoft.Data.SqlClient;
using Model;
using System;
using System.Data;
using System.Data.Common;
using System.Text;
using System.Threading.Tasks;namespace DataProcess
{public class Access{public static List<CmdData> list = new List<CmdData>();#region 基础basic/// <summary>/// 单增/// </summary>public static async Task<bool> Add<T>(T entity){string SqlStr = SqlBuilder.InsertSQL<T>(entity);//语句SqlParameter[] parameter = SqlParameters.NoID(entity);//参数list.Add(new CmdData(SqlStr, parameter));int rst = (int)await SqlHelper.MyTran(list, "Edit", true);return rst > 0;}/// <summary>/// 单改/// </summary>public static async Task<bool> Update<T>(T entity){string SqlStr = SqlBuilder.UpdateSQL<T>(entity);SqlParameter[] parameters = SqlParameters.HaveId(entity);list.Add(new CmdData(SqlStr, parameters));int rst = (int)await SqlHelper.MyTran(list, "Edit", true);return rst > 0;}/// <summary>/// 单删/// </summary>public static async Task<bool> Delete<T>(T entity){string SqlStr = SqlBuilder.DeleteSQL<T>(entity);SqlParameter[] parameters = SqlParameters.OnlyID(entity);list.Add(new CmdData(SqlStr, parameters));int rst = (int)await SqlHelper.MyTran(list, "Edit", true);return rst > 0;}/// <summary>/// 单查=>Entity/// </summary>public static async Task<T> GetModel<T>(int id) where T : new(){string SqlStr = SqlBuilder.SelectSQL<T>(id.ToString());list.Add(new CmdData(SqlStr, null));DataSet ds = (DataSet)await SqlHelper.MyTran(list, "QueryDs", true);if (ds.Tables[0].Rows.Count > 0)return SqlOrm.ToEntity<T>(ds.Tables[0].Rows[0]);else return new T();}#endregion#region 扩展 Extend/// <summary>/// 批量增/// </summary>public static async Task<bool> Adds<T>(List<T> listT){foreach (var entity in listT){string SqlStr = SqlBuilder.InsertSQL<T>(entity);//语句SqlParameter[] parameter = SqlParameters.NoID(entity);//参数list.Add(new CmdData(SqlStr, parameter));}int rst = (int)await SqlHelper.MyTran(list, "Edit", true);return rst > 0;}/// <summary>/// 批改/// </summary>public static async Task<bool> Updates<T>(List<T> listT){foreach (var entity in listT){string SqlStr = SqlBuilder.UpdateSQL<T>(entity);SqlParameter[] parameter = SqlParameters.HaveId(entity);list.Add(new CmdData(SqlStr, parameter));}int rst = (int)await SqlHelper.MyTran(list, "Edit", true);return rst > 0;}/// <summary>/// 批删/// </summary>public static async Task<bool> Deletes<T>(List<T> listT){foreach (var entity in listT){string SqlStr = SqlBuilder.DeleteSQL<T>(entity);SqlParameter[] parameters = SqlParameters.OnlyID(entity);list.Add(new CmdData(SqlStr, parameters));}int rst = (int)await SqlHelper.MyTran(list, "Edit", true);return rst > 0;}/// <summary>/// 单增 返回ID/// </summary>public static async Task<int> Add_ID<T>(T entity){string SqlStr = SqlBuilder.InsertSQL_id<T>(entity);//语句SqlParameter[] parameters = SqlParameters.NoID(entity);//参数list.Add(new CmdData(SqlStr, parameters));return (int)await SqlHelper.MyTran(list, "QueryInt", true);}/// <summary>/// 条件查询/// </summary>/// <returns></returns>public static async Task<DataSet> GetList<T>(string strWhere) where T : new(){string SqlStr = SqlBuilder.ListSQL<T>(strWhere);list.Add(new CmdData(SqlStr, null));return (DataSet)await SqlHelper.MyTran(list, "QueryDs", true);}/// <summary>/// 条件查询=》多Entity/// </summary>public static async Task<List<T>> GetModelList<T>(string strWhere) where T : new(){DataSet ds = await GetList<T>(strWhere);List<T> rst = new List<T>();if (ds.Tables[0].Rows.Count > 0){ds.Tables[0].Rows.Cast<DataRow>().ToList().ForEach(p =>{rst.Add(SqlOrm.ToEntity<T>(p));});}return rst;}/// <summary>/// 条件查询=》条数/// </summary>public static async Task<int> RecordCount<T>(string strWhere) where T : new(){string SqlStr = SqlBuilder.ListSQL<T>(strWhere);list.Add(new CmdData(SqlStr, null));return (int)await SqlHelper.MyTran(list, "QueryInt", true);}/// <summary>/// 分页/// </summary>public static async Task<DataSet> GetListByPage<T>(string strWhere, string orderby, int startIndex, int endIndex) where T : new(){string SqlStr = SqlBuilder.ByPageSQL<T>(strWhere, orderby, startIndex, endIndex);list.Add(new CmdData(SqlStr, null));return (DataSet)await SqlHelper.MyTran(list, "QueryDs", true);}/// <summary>/// 自定义查询/// </summary>public static async Task<DataSet> CustomQuery<T>(string [] param, string strWhere) where T : new(){string SqlStr = SqlBuilder.CustomQuerySQL<T>(param,strWhere);list.Add(new CmdData(SqlStr, null));return (DataSet)await SqlHelper.MyTran(list, "QueryDs", true);}/// <summary>/// 存储过程/// </summary>public static async Task<DataSet> ExecProc(StoredProcs proc){// return (DataSet)await SqlHelper.MyTran(SqlStr, null, "Query", 1);string SqlStr = SqlBuilder.ProcSQL(proc);//语句list.Add(new CmdData(SqlStr, null));return (DataSet)await SqlHelper.MyTran(list, "Edit", false);//不启动事务}#endregion}
}
八、总结
5个类完成了自己在 .net8 中的ORM. 不是太复杂。
我的想法是前端传回json到API ,解析成modelList 完成批量的增删改,联表这种数据量大的就丢到存储过程里去把。
//调用存储过程示例
public static async Task<DataSet> MyProc()
{StoredProc proc = new StoredProc();proc.Name = "SP_Test";proc.param.Add(new ProcParam("createtime", "2025-0-13"));proc.param.Add(new ProcParam("name", "张三"));proc.param.Add(new ProcParam("class", "2年纪"));return await Access.ExecProc(proc);
}
//批量修改示例public static async Task<bool> Updates<Users>(string JsonStr){List<Users> List = JsonConvert.DeserializeObject<List<Users>>(JsonStr);return await Access.Updates<Users>(List);}
//分页查询示例public static async Task<string> GetListByPage<User>(){DataSet ds= await Access.GetListByPage<User> ("name like '%张%'"," createtme desc", 101, 200);string JsonStr=JsonConvert.SerializeObject(ds.tables[0]);return JsonStr;}