ASP.NET Core 中的 ORM 之 Dapper

Dapper 簡介

Dapper是.NET的一款輕量級ORM工具(GitHub),也可稱爲簡單對象映射器。在速度方面擁有微型ORM之王的稱號。
它是半自動的,也就是說實體類和SQL語句都要本身寫,但它提供自動對象映射。是經過對IDbConnection接口的擴展來操做數據庫的。git

優勢github

  • 輕量,只有一個文件
  • 性能高,Dapper的速度接近與IDataReader,取列表的數據超過了DataTable。
  • 支持多種數據庫。Dapper能夠在全部Ado.net Providers下工做,包括sqlite, sqlce, firebird, oracle, MySQL, PostgreSQL and SQL Server
  • 使用Dapper能夠自動進行對象映射,經過Emit反射IDataReader的序列隊列,來快速的獲得和產生對象

使用 Dapper

下面簡單建立一個Web API應用並經過Dapper訪問MySQL數據。sql

  1. 建立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);
  2. 建立Web API應用並添加NuGet引用api

    Install-Package MySql.Data
    Install-Package Dapper
  3. 新建一個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; }
    }
  4. 新建一個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;
        }
    }
  5. 建立簡單的倉儲接口和類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);
    }
  6. 在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();
        }
    }
  7. 測試API是否能夠正常工做

  8. 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();
        }
    }
  9. 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 Contrib擴展Dapper提供了CRUD的方法

  • Get
  • GetAll
  • Insert
  • Update
  • Delete
  • DeleteAll
  1. 添加NuGet引用Dapper.Contrib

    Install-Package Dapper.Contrib
  2. 爲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; }
    }
  3. 增長一個新的倉儲類繼承

    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語句。

  4. 其餘一些開源的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

引入工做單元 Unit of Work

倉儲模式每每須要工做單元模式的介入來負責一系列倉儲對象的持久化,確保數據完整性。網上關於工做單元模式的實現方式有多種,但其本質都是工做單元類經過建立一個全部倉儲共享的數據庫上下文對象,來組織多個倉儲對象。

網上的一些實現方式:

  • 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;
        }
    }
  • 怎麼使用
    1. 建立一個本身的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;
          }
      }
    2. 倉儲類裏面添加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;
          }
      }
    3. 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);
      }
    4. 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();
          }
      }

源代碼

Github

參考

相關文章
相關標籤/搜索