Dapper in .Net Core
Posted 大道无术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dapper in .Net Core相关的知识,希望对你有一定的参考价值。
一、前言
关于什么是Dapper(详细入口),在此不做赘述;本文仅对Dapper在.Net Core中的使用作扼要说明,所陈代码以示例讲解为主,乃抛砖引玉,开发者可根据自身需要进行扩展和调整;其中如有疏漏之处,望不吝斧正。
二、Dapper环境搭建
当前以.Net Core WebAPI或MVC项目为例,框架版本为.NET 5.0,相关NuGet包引用如下:
Install-Package Dapper
Install-Package Dapper.Contrib
Install-Package Dapper.SqlBuilder
Install-Package System.Data.SqlClient
其中Dapper.Contrib和Dapper.SqlBuilder为Dapper的扩展,当然,Dapper的扩展还有如Dapper.Rainbow等其他包,根据自身需要引用,对相关引用作下说明:
- Dapper:不言而喻;
- Dapper.Contrib:可使用对象进行数据表的增删改查,免却SQL语句的编写;
- Dapper.SqlBuilder:可以方便动态构建SQL语句,如Join、SELECT、Where、OrderBy等等;
- System.Data.SqlClient:由于示例数据库为Sql Server,如MySql则引用MySql.Data;
对于Dapper.Contrib实体配置选项,以Product类为例,作扼要说明如下:
[Table("Product")] public class Product [Key] public int Id get; set; public string Name get; set; public string Description get; set; public decimal Price get; set; public DateTime CreateTime get; set;
对于实体配置项,有如下几个主要项:
- Table:指定数据库表名,可忽略;
- Key:指定为自动增长主键;
- ExplicitKey:指定非自动增长主键,如guid;
- Computed:计算列属性,Insert、Update操作将忽略此列;
- Write:是否可写入,true/false,如[Write(false)],false时Insert、Update操作将忽略此列,比如可扩展局部类作数据表额外查询字段使用;
对于数据表对象实体,可结合T4模板生成即可。
三、Dapper封装
关于Dapper数据访问,这里参考Github上的某示例(入口:https://github.com/EloreTec/UnitOfWorkWithDapper),作修改调整封装如下:
定义DapperDBContext类
public abstract class DapperDBContext : IContext private IDbConnection _connection; private IDbTransaction _transaction; private int? _commandTimeout = null; private readonly DapperDBContextOptions _options; public bool IsTransactionStarted get; private set; protected abstract IDbConnection CreateConnection(string connectionString); protected DapperDBContext(IOptions<DapperDBContextOptions> optionsAccessor) _options = optionsAccessor.Value; _connection = CreateConnection(_options.Configuration); _connection.Open(); DebugPrint("Connection started."); #region Transaction public void BeginTransaction() if (IsTransactionStarted) throw new InvalidOperationException("Transaction is already started."); _transaction = _connection.BeginTransaction(); IsTransactionStarted = true; DebugPrint("Transaction started."); public void Commit() if (!IsTransactionStarted) throw new InvalidOperationException("No transaction started."); _transaction.Commit(); _transaction = null; IsTransactionStarted = false; DebugPrint("Transaction committed."); public void Rollback() if (!IsTransactionStarted) throw new InvalidOperationException("No transaction started."); _transaction.Rollback(); _transaction.Dispose(); _transaction = null; IsTransactionStarted = false; DebugPrint("Transaction rollbacked and disposed."); #endregion Transaction #region Dapper.Contrib.Extensions public async Task<T> GetAsync<T>(int id) where T : class, new() return await _connection.GetAsync<T>(id, _transaction, _commandTimeout); public async Task<T> GetAsync<T>(string id) where T : class, new() return await _connection.GetAsync<T>(id, _transaction, _commandTimeout); public async Task<IEnumerable<T>> GetAllAsync<T>() where T : class, new() return await _connection.GetAllAsync<T>(); public long Insert<T>(T model) where T : class, new() return _connection.Insert<T>(model, _transaction, _commandTimeout); public async Task<int> InsertAsync<T>(T model) where T : class, new() return await _connection.InsertAsync<T>(model, _transaction, _commandTimeout); public bool Update<T>(T model) where T : class, new() return _connection.Update<T>(model, _transaction, _commandTimeout); public async Task<bool> UpdateAsync<T>(T model) where T : class, new() return await _connection.UpdateAsync<T>(model, _transaction, _commandTimeout); public async Task<Page<T>> PageAsync<T>(long pageIndex, long pageSize, string sql, object param = null) DapperPage.BuildPageQueries((pageIndex - 1) * pageSize, pageSize, sql, out string sqlCount, out string sqlPage); var result = new Page<T> CurrentPage = pageIndex, ItemsPerPage = pageSize, TotalItems = await _connection.ExecuteScalarAsync<long>(sqlCount, param) ; result.TotalPages = result.TotalItems / pageSize; if ((result.TotalItems % pageSize) != 0) result.TotalPages++; result.Items = await _connection.QueryAsync<T>(sqlPage, param); return result; #endregion #region Dapper Execute & Query public int ExecuteScalar(string sql, object param = null, CommandType commandType = CommandType.Text) return _connection.ExecuteScalar<int>(sql, param, _transaction, _commandTimeout, commandType); public async Task<int> ExecuteScalarAsync(string sql, object param = null, CommandType commandType = CommandType.Text) return await _connection.ExecuteScalarAsync<int>(sql, param, _transaction, _commandTimeout, commandType); public int Execute(string sql, object param = null, CommandType commandType = CommandType.Text) return _connection.Execute(sql, param, _transaction, _commandTimeout, commandType); public async Task<int> ExecuteAsync(string sql, object param = null, CommandType commandType = CommandType.Text) return await _connection.ExecuteAsync(sql, param, _transaction, _commandTimeout, commandType); public IEnumerable<T> Query<T>(string sql, object param = null, CommandType commandType = CommandType.Text) return _connection.Query<T>(sql, param, _transaction, true, _commandTimeout, commandType); public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object param = null, CommandType commandType = CommandType.Text) return await _connection.QueryAsync<T>(sql, param, _transaction, _commandTimeout, commandType); public T QueryFirstOrDefault<T>(string sql, object param = null, CommandType commandType = CommandType.Text) return _connection.QueryFirstOrDefault<T>(sql, param, _transaction, _commandTimeout, commandType); public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object param = null, CommandType commandType = CommandType.Text) return await _connection.QueryFirstOrDefaultAsync<T>(sql, param, _transaction, _commandTimeout, commandType); public IEnumerable<TReturn> Query<TFirst, TSecond, TReturn>(string sql, Func<TFirst, TSecond, TReturn> map, object param = null, string splitOn = "Id", CommandType commandType = CommandType.Text) return _connection.Query(sql, map, param, _transaction, true, splitOn, _commandTimeout, commandType); public async Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TReturn>(string sql, Func<TFirst, TSecond, TReturn> map, object param = null, string splitOn = "Id", CommandType commandType = CommandType.Text) return await _connection.QueryAsync(sql, map, param, _transaction, true, splitOn, _commandTimeout, commandType); public async Task<SqlMapper.GridReader> QueryMultipleAsync(string sql, object param = null, CommandType commandType = CommandType.Text) return await _connection.QueryMultipleAsync(sql, param, _transaction, _commandTimeout, commandType); #endregion Dapper Execute & Query public void Dispose() if (IsTransactionStarted) Rollback(); _connection.Close(); _connection.Dispose(); _connection = null; DebugPrint("Connection closed and disposed."); private void DebugPrint(string message) #if DEBUG Debug.Print(">>> UnitOfWorkWithDapper - Thread 0: 1", Thread.CurrentThread.ManagedThreadId, message); #endif
public class DapperDBContextOptions : IOptions<DapperDBContextOptions> public string Configuration get; set; DapperDBContextOptions IOptions<DapperDBContextOptions>.Value get return this; public interface IContext : IDisposable bool IsTransactionStarted get; void BeginTransaction(); void Commit(); void Rollback();
以上代码涵盖了Dapper访问数据库的基本操作,分同步和异步,其中大部分不作赘述,着重说下分页部分;
异步分页构建(PageAsync)
分页这里为方便调用,只需传入要查询的Sql语句(如:SELECT * FROM Table,必须带Order BY)、页索引、页大小即可;
至于具体如何构建的,这里参照某小型ORM工具PetaPoco,抽取相关代码如下,有兴趣的同学也可以自行改造:
public class Page<T> /// <summary> /// The current page number contained in this page of result set /// </summary> public long CurrentPage get; set; /// <summary> /// The total number of pages in the full result set /// </summary> public long TotalPages get; set; /// <summary> /// The total number of records in the full result set /// </summary> public long TotalItems get; set; /// <summary> /// The number of items per page /// </summary> public long ItemsPerPage get; set; /// <summary> /// The actual records on this page /// </summary> public IEnumerable<T> Items get; set; //public List<T> Items get; set; public class DapperPage public static void BuildPageQueries(long skip, long take, string sql, out string sqlCount, out string sqlPage) // Split the SQL if (!PagingHelper.SplitSQL(sql, out PagingHelper.SQLParts parts)) throw new Exception("Unable to parse SQL statement for paged query"); sqlPage = BuildPageSql.BuildPageQuery(skip, take, parts); sqlCount = parts.sqlCount; static class BuildPageSql public static string BuildPageQuery(long skip, long take, PagingHelper.SQLParts parts) parts.sqlSelectRemoved = PagingHelper.rxOrderBy.Replace(parts.sqlSelectRemoved, "", 1); if (PagingHelper.rxDistinct.IsMatch(parts.sqlSelectRemoved)) parts.sqlSelectRemoved = "peta_inner.* FROM (SELECT " + parts.sqlSelectRemoved + ") peta_inner"; var sqlPage = string.Format("SELECT * FROM (SELECT ROW_NUMBER() OVER (0) peta_rn, 1) peta_paged WHERE peta_rn>2 AND peta_rn<=3", parts.sqlOrderBy ?? "ORDER BY (SELECT NULL)", parts.sqlSelectRemoved, skip, skip + take); //args = args.Concat(new object[] skip, skip + take ).ToArray(); return sqlPage; //SqlServer 2012及以上 public static string BuildPageQuery2(long skip, long take, PagingHelper.SQLParts parts) parts.sqlSelectRemoved = PagingHelper.rxOrderBy.Replace(parts.sqlSelectRemoved, "", 1); if (PagingHelper.rxDistinct.IsMatch(parts.sqlSelectRemoved)) parts.sqlSelectRemoved = "peta_inner.* FROM (SELECT " + parts.sqlSelectRemoved + ") peta_inner"; var sqlOrderBy = parts.sqlOrderBy ?? "ORDER BY (SELECT NULL)"; var sqlPage = $"SELECT parts.sqlSelectRemoved sqlOrderBy OFFSET skip ROWS FETCH NEXT take ROWS ONLY"; return sqlPage; static class PagingHelper public struct SQLParts public string sql; public string sqlCount; public string sqlSelectRemoved; public string sqlOrderBy; public static bool SplitSQL(string sql, out SQLParts parts) parts.sql = sql; parts.sqlSelectRemoved = null; parts.sqlCount = null; parts.sqlOrderBy = null; // Extract the columns from "SELECT <whatever> FROM" var m = rxColumns.Match(sql); if (!m.Success) return false; // Save column list and replace with COUNT(*) Group g = m.Groups[1]; parts.sqlSelectRemoved = sql.Substring(g.Index); if (rxDistinct.IsMatch(parts.sqlSelectRemoved)) parts.sqlCount = sql.Substring(0, g.Index) + "COUNT(" + m.Groups[1].ToString().Trim() + ") " + sql.Substring(g.Index + g.Length); else parts.sqlCount = sql.Substring(0, g.Index) + "COUNT(*) " + sql.Substring(g.Index + g.Length); // Look for the last "ORDER BY <whatever>" clause not part of a ROW_NUMBER expression m = rxOrderBy.Match(parts.sqlCount); if (!m.Success) parts.sqlOrderBy = null; else g = m.Groups[0]; parts.sqlOrderBy = g.ToString(); parts.sqlCount = parts.sqlCount.Substring(0, g.Index) + parts.sqlCount.Substring(g.Index + g.Length); return true; public static Regex rxColumns = new Regex(@"\\A\\s*SELECT\\s+((?:\\((?>\\((?<depth>)|\\)(?<-depth>)|.?)*(?(depth)(?!))\\)|.)*?)(?<!,\\s+)\\bFROM\\b", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled); public static Regex rxOrderBy = new Regex(@"\\bORDER\\s+BY\\s+(?!.*?(?:\\)|\\s+)AS\\s)(?:\\((?>\\((?<depth>)|\\)(?<-depth>)|.?)*(?(depth)(?!))\\)|[\\w\\(\\)\\.])+(?:\\s+(?:ASC|DESC))?(?:\\s*,\\s*(?:\\((?>\\((?<depth>)|\\)(?<-depth>)|.?)*(?(depth)(?!))\\)|[\\w\\(\\)\\.])+(?:\\s+(?:ASC|DESC))?)*", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled); public static Regex rxDistinct = new Regex(@"\\ADISTINCT\\s", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled);
对于构建分页语句,分别示例BuildPageQuery和BuildPageQuery2,前者为通过ROW_NUMBER进行分页(针对SqlServer2005、2008),后者通过OFFSET、FETCH分页(针对SqlServer2012及以上版本),相关辅助操作类一览便知,如果使用MySql数据库,可酌情自行封装;
至于Where查询的进一步封装,有兴趣的也可兑Dapper lamada查询进行扩展。
定义工作单元与事务
public interface IUnitOfWork : IDisposable void SaveChanges(); public interface IUnitOfWorkFactory IUnitOfWork Create(); public class UnitOfWork : IUnitOfWork private readonly IContext _context; public UnitOfWork(IContext context) _context = context; _context.BeginTransaction(); public void SaveChanges() if (!_context.IsTransactionStarted) throw new InvalidOperationException("Transaction have already been commited or disposed."); _context.Commit(); public void Dispose() if (_context.IsTransactionStarted) _context.Rollback(); public class DapperUnitOfWorkFactory : IUnitOfWorkFactory private readonly DapperDBContext _context; public DapperUnitOfWorkFactory(DapperDBContext context) _context = context; public IUnitOfWork Create() return new UnitOfWork(_context);
定义数据仓储
#region Product public partial interface IProductRepository Task<Product> GetAsync(int id); Task<IEnumerable<Product>> GetAllAsync(); long Insert(Product model); Task<int> InsertAsync(Product model); bool Update(Product model); Task<bool> UpdateAsync(Product model); int Count(string where, object param = null); Task<int> CountAsync(string where, object param = null); bool Exists(string where, object param = null); Task<bool> ExistsAsync(string where, object param = null); Product FirstOrDefault(string where, object param = null); Task<Product> FirstOrDefaultAsync(string where, object param = null); T FirstOrDefault<T>(string sql, object param = null); Task<T> FirstOrDefaultAsync<T>(string sql, object param = null); IEnumerable<Product> Fetch(SqlBuilder where); Task<IEnumerable<Product>> FetchAsync(SqlBuilder where); IEnumerable<T> Fetch<T>(string sql, SqlBuilder where, bool orderBy = true); Task<IEnumerable<T>> FetchAsync<T>(string sql, SqlBuilder where, bool orderBy = true); Task<Page<Product>> PageAsync(long pageIndex, long pageSize, SqlBuilder builder); Task<Page<T>> PageAsync<T>(string sql, long pageIndex, long pageSize, SqlBuilder builder); Task<SqlMapper.GridReader> QueryMultipleAsync(string sql, object param = null); public partial class ProductRepository : IProductRepository private readonly DapperDBContext _context; public ProductRepository(DapperDBContext context) _context = context; public async Task<Product> GetAsync(int id) return await _context.GetAsync<Product>(id); public async Task<IEnumerable<Product>> GetAllAsync() return await _context.GetAllAsync<Product>(); public long Insert(Product model) return _context.Insert<Product>(model); public async Task<int> InsertAsync(Product model) return await _context.InsertAsync<Product>(model); public bool Update(Product model) return _context.Update<Product>(model); public async Task<bool> UpdateAsync(Product model) return await _context.UpdateAsync<Product>(model); public int Count(string where, object param = null) string strSql = $"SELECT COUNT(1) FROM Product where"; return _context.ExecuteScalar(strSql, param); public async Task<int> CountAsync(string where, object param = null) string strSql = $"SELECT COUNT(1) FROM Product where"; return await _context.ExecuteScalarAsync(strSql, param); public bool Exists(string where, object param = null) string strSql = $"SELECT TOP 1 1 FROM Product where"; var count = _context.ExecuteScalar(strSql, param); return count > 0; public async Task<bool> ExistsAsync(string where, object param = null) string strSql = $"SELECT TOP 1 1 FROM Product where"; var count = await _context.ExecuteScalarAsync(strSql, param); return count > 0; public Product FirstOrDefault(string where, object param = null) string strSql = $"SELECT TOP 1 * FROM Product where"asp.net core 使用Mysql和Dapper