Dapper是.NET的一款輕量級ORM工具(GitHub),也可稱爲簡單對象映射器。在速度方面擁有微型ORM之王的稱號。
它是半自動的,也就是說實體類和SQL語句都要本身寫,但它提供自動對象映射。是經過對IDbConnection接口的擴展來操做數據庫的。git
優勢:github
下面簡單建立一個Web API應用並經過Dapper訪問MySQL數據。sql
建立MySQL測試數據數據庫
CREATE SCHEMA `ormdemo` ; CREATE TABLE `ormdemo`.`category` ( `Id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(45) NOT NULL, PRIMARY KEY (`Id`)); CREATE TABLE `ormdemo`.`product` ( `Id` INT NOT NULL AUTO_INCREMENT, `Name` VARCHAR(45) NOT NULL, `Price` DECIMAL(19,2) NULL, `Quantity` INT NULL, `CategoryId` INT NOT NULL, PRIMARY KEY (`Id`), INDEX `fk_product_category_idx` (`CategoryId` ASC), CONSTRAINT `fk_product_category` FOREIGN KEY (`CategoryId`) REFERENCES `ormdemo`.`category` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION); INSERT INTO `ormdemo`.`category` (`Name`) VALUES("Phones"); INSERT INTO `ormdemo`.`category` (`Name`) VALUES("Computers"); INSERT INTO `ormdemo`.`product` (`Name`,`Price`,`Quantity`,`CategoryId`) VALUES("iPhone8",4999.99,10,1); INSERT INTO `ormdemo`.`product` (`Name`,`Price`,`Quantity`,`CategoryId`) VALUES("iPhone7",2999.99,10,1); INSERT INTO `ormdemo`.`product` (`Name`,`Price`,`Quantity`,`CategoryId`) VALUES("HP750",6000.00,5,2); INSERT INTO `ormdemo`.`product` (`Name`,`Price`,`Quantity`,`CategoryId`) VALUES("HP5000",12000.00,10,2);
建立Web API應用並添加NuGet引用api
Install-Package MySql.Data Install-Package Dapper
新建一個Product類mvc
public class Category { public int Id { get; set; } public string Name { get; set; } } public class Product { public int Id { get; set; } public string Name { get; set; } public int Quantity { get; set; } public decimal Price { get; set; } public int CategoryId { get; set; } public virtual Category Category { get; set; } }
新建一個DBConfig類用於建立並返回數據庫鏈接oracle
using MySql.Data.MySqlClient; using System.Data; using System.Configuration; public class DBConfig { //ConfigurationManager.ConnectionStrings["Connection"].ConnectionString; private static string DefaultSqlConnectionString = @"server=127.0.0.1;database=ormdemo;uid=root;pwd=Open0001;SslMode=none;"; public static IDbConnection GetSqlConnection(string sqlConnectionString = null) { if (string.IsNullOrWhiteSpace(sqlConnectionString)) { sqlConnectionString = DefaultSqlConnectionString; } IDbConnection conn = new MySqlConnection(sqlConnectionString); conn.Open(); return conn; } }
建立簡單的倉儲接口和類app
public interface IProductRepository { Task<bool> AddAsync(Product prod); Task<IEnumerable<Product>> GetAllAsync(); Task<Product> GetByIDAsync(int id); Task<bool> DeleteAsync(int id); Task<bool> UpdateAsync(Product prod); }
public class ProductRepository : IProductRepository { public async Task<IEnumerable<Product>> GetAllAsync() { using (IDbConnection conn = DBConfig.GetSqlConnection()) { return await conn.QueryAsync<Product>(@"SELECT Id ,Name ,Quantity ,Price ,CategoryId FROM Product"); } } public async Task<Product> GetByIDAsync(int id) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { string sql = @"SELECT Id ,Name ,Quantity ,Price ,CategoryId FROM Product WHERE Id = @Id"; return await conn.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id }); } } public async Task<bool> AddAsync(Product prod) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { string sql = @"INSERT INTO Product (Name ,Quantity ,Price ,CategoryId) VALUES (@Name ,@Quantity ,@Price ,@CategoryId)"; return await conn.ExecuteAsync(sql, prod) > 0; } } public async Task<bool> UpdateAsync(Product prod) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { string sql = @"UPDATE Product SET Name = @Name ,Quantity = @Quantity ,Price= @Price ,CategoryId= @CategoryId WHERE Id = @Id"; return await conn.ExecuteAsync(sql, prod) > 0; } } public async Task<bool> DeleteAsync(int id) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { string sql = @"DELETE FROM Product WHERE Id = @Id"; return await conn.ExecuteAsync(sql, new { Id = id }) > 0; } } }
在Startup ConfigureServices方法裏面配置依賴注入async
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IProductRepository, ProductRepository>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
在Controller裏面調用倉儲方法
[Route("api/[controller]")] [ApiController] public class ProductController : ControllerBase { private readonly IProductRepository _productRepository; public ProductController(IProductRepository productRepository) { _productRepository = productRepository; } [HttpGet] public async Task<IActionResult> Get() { var data = await _productRepository.GetAllAsync(); return Ok(data); } [HttpGet("{id}")] public async Task<IActionResult> Get(int id) { var data = await _productRepository.GetByIDAsync(id); return Ok(data); } [HttpPost] public async Task<IActionResult> Post([FromBody] Product prod) { if (!ModelState.IsValid) { return BadRequest(ModelState); } await _productRepository.AddAsync(prod); return NoContent(); } [HttpPut("{id}")] public async Task<IActionResult> Put(int id, [FromBody] Product prod) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var model = await _productRepository.GetByIDAsync(id); model.Name = prod.Name; model.Quantity = prod.Quantity; model.Price = prod.Price; await _productRepository.UpdateAsync(model); return NoContent(); } [HttpDelete("{id}")] public async Task<IActionResult> Delete(int id) { await _productRepository.DeleteAsync(id); return NoContent(); } }
測試API是否能夠正常工做
Dapper對存儲過程和事務的支持
存儲過程
using (var connection = My.ConnectionFactory()) { connection.Open(); var affectedRows = connection.Execute(sql, new {Kind = InvoiceKind.WebInvoice, Code = "Single_Insert_1"}, commandType: CommandType.StoredProcedure); My.Result.Show(affectedRows); }
事務
using (var connection = My.ConnectionFactory()) { connection.Open(); using (var transaction = connection.BeginTransaction()) { var affectedRows = connection.Execute(sql, new {CustomerName = "Mark"}, transaction: transaction); transaction.Commit(); } }
Dapper對多表映射的支持
var selectAllProductWithCategorySQL = @"select * from product p inner join category c on c.Id = p.CategoryId Order by p.Id"; var allProductWithCategory = connection.Query<Product, Category, Product>(selectAllProductWithCategorySQL, (prod, cg) => { prod.Category = cg; return prod; });
Dapper Contrib擴展Dapper提供了CRUD的方法
添加NuGet引用Dapper.Contrib
Install-Package Dapper.Contrib
爲Product類添加數據註解
[Table("Product")] public class Product { [Key] public int Id { get; set; } public string Name { get; set; } public int Quantity { get; set; } public decimal Price { get; set; } public int CategoryId { get; set; } public virtual Category Category { get; set; } }
增長一個新的倉儲類繼承
public class ContribProductRepository : IProductRepository { public async Task<bool> AddAsync(Product prod) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { return await conn.InsertAsync(prod) > 0; } } public async Task<IEnumerable<Product>> GetAllAsync() { using (IDbConnection conn = DBConfig.GetSqlConnection()) { return await conn.GetAllAsync<Product>(); } } public async Task<Product> GetByIDAsync(int id) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { return await conn.GetAsync<Product>(id); } } public async Task<bool> DeleteAsync(int id) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { var entity = await conn.GetAsync<Product>(id); return await conn.DeleteAsync(entity); } } public async Task<bool> UpdateAsync(Product prod) { using (IDbConnection conn = DBConfig.GetSqlConnection()) { return await conn.UpdateAsync(prod); } } }
修改Startup ConfigureServices方法裏面配置依賴注入
services.AddTransient<IProductRepository, ContribProductRepository>();
測試,這樣能夠少寫了很多基本的SQL語句。
其餘一些開源的Dapper擴展
類庫 | 提供的方法 |
---|---|
Dapper.SimpleCRUD | Get GetList GetListPaged Insert Update Delete DeleteList RecordCount |
Dapper Plus | Bulk Insert Bulk Delete Bulk Update Bulk Merge Bulk Action Async Bulk Also Action Bulk Then Action |
Dapper.FastCRUD | Get Find Insert Update BulkUpdate Delete BulkDelete Count |
Dapper.Mapper | Multi-mapping |
倉儲模式每每須要工做單元模式的介入來負責一系列倉儲對象的持久化,確保數據完整性。網上關於工做單元模式的實現方式有多種,但其本質都是工做單元類經過建立一個全部倉儲共享的數據庫上下文對象,來組織多個倉儲對象。
網上的一些實現方式:
Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application
微軟以前給出的一個示例,倉儲類作爲工做單元的變量,並經過工做單元傳入一致的context參數建立。
public class UnitOfWork : IDisposable { private SchoolContext context = new SchoolContext(); private GenericRepository<Department> departmentRepository; private GenericRepository<Course> courseRepository; public GenericRepository<Department> DepartmentRepository { get { if (this.departmentRepository == null) { this.departmentRepository = new GenericRepository<Department>(context); } return departmentRepository; } } public GenericRepository<Course> CourseRepository { get { if (this.courseRepository == null) { this.courseRepository = new GenericRepository<Course>(context); } return courseRepository; } } public void Save() { context.SaveChanges(); } private bool disposed = false; protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { context.Dispose(); } } this.disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }
DDD 領域驅動設計-談談 Repository、IUnitOfWork 和 IDbContext 的實踐
博客園一位大神的總結,最終採用的方案是倉儲類負責查詢,工做單元類負責增刪改等數據持久化操做。
優缺點不做討論,適合本身的就是最好的,這裏採用了另一種實現方式:
定義DapperDBContext
public interface IContext : IDisposable { bool IsTransactionStarted { get; } void BeginTransaction(); void Commit(); void Rollback(); } 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 Execute & Query public async Task<int> ExecuteAsync(string sql, object param = null, CommandType commandType = CommandType.Text) { return await _connection.ExecuteAsync(sql, param, _transaction, _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 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 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); } #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 } }
定義UnitOfWork
public interface IUnitOfWork : IDisposable { void SaveChanges(); } 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(); } }
定義UnitOfWorkFactory
public interface IUnitOfWorkFactory { IUnitOfWork Create(); } public class DapperUnitOfWorkFactory : IUnitOfWorkFactory { private readonly DapperDBContext _context; public DapperUnitOfWorkFactory(DapperDBContext context) { _context = context; } public IUnitOfWork Create() { return new UnitOfWork(_context); } }
定義服務擴展
public class DapperDBContextOptions : IOptions<DapperDBContextOptions> { public string Configuration { get; set; } DapperDBContextOptions IOptions<DapperDBContextOptions>.Value { get { return this; } } } public static class DapperDBContextServiceCollectionExtensions { public static IServiceCollection AddDapperDBContext<T>(this IServiceCollection services, Action<DapperDBContextOptions> setupAction) where T : DapperDBContext { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (setupAction == null) { throw new ArgumentNullException(nameof(setupAction)); } services.AddOptions(); services.Configure(setupAction); services.AddScoped<DapperDBContext, T>(); services.AddScoped<IUnitOfWorkFactory, DapperUnitOfWorkFactory>(); return services; } }
建立一個本身的Context並繼承DapperDBContext。下面測試的TestDBContext是採用MySQL數據庫並返回MySqlConnection。
public class TestDBContext : DapperDBContext { public TestDBContext(IOptions<DapperDBContextOptions> optionsAccessor) : base(optionsAccessor) { } protected override IDbConnection CreateConnection(string connectionString) { IDbConnection conn = new MySqlConnection(connectionString); return conn; } }
倉儲類裏面添加DapperDBContext引用
public class UowProductRepository : IProductRepository { private readonly DapperDBContext _context; public UowProductRepository(DapperDBContext context) { _context = context; } public async Task<Product> GetByIDAsync(int id) { string sql = @"SELECT Id ,Name ,Quantity ,Price ,CategoryId FROM Product WHERE Id = @Id"; return await _context.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id }); } public async Task<bool> AddAsync(Product prod) { string sql = @"INSERT INTO Product (Name ,Quantity ,Price ,CategoryId) VALUES (@Name ,@Quantity ,@Price ,@CategoryId)"; return await _context.ExecuteAsync(sql, prod) > 0; } }
Startup裏面註冊服務
public void ConfigureServices(IServiceCollection services) { services.AddDapperDBContext<TestDBContext>(options => { options.Configuration = @"server=127.0.0.1;database=ormdemo;uid=root;pwd=password;SslMode=none;"; }); services.AddTransient<IProductRepository, UowProductRepository>(); services.AddTransient<ICategoryRepository, UowCategoryRepository>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
Controller調用
public class ProductController : ControllerBase { private readonly IUnitOfWorkFactory _uowFactory; private readonly IProductRepository _productRepository; private readonly ICategoryRepository _categoryRepository; public ProductController(IUnitOfWorkFactory uowFactory, IProductRepository productRepository, ICategoryRepository categoryRepository) { _uowFactory = uowFactory; _productRepository = productRepository; _categoryRepository = categoryRepository; } [HttpGet("{id}")] public async Task<IActionResult> Get(int id) { var data = await _productRepository.GetByIDAsync(id); return Ok(data); } [HttpPost] public async Task<IActionResult> Post([FromBody] Product prod) { if (!ModelState.IsValid) { return BadRequest(ModelState); } //await _productRepository.AddAsync(prod); using (var uow = _uowFactory.Create()) { await _productRepository.AddAsync(prod); uow.SaveChanges(); } return NoContent(); } }