1. 程式人生 > >NET Core 實戰 Dapper 擴展數據訪問

NET Core 實戰 Dapper 擴展數據訪問

討論 擴展 代碼實現 arch 資源 acl 靜態頁面 枚舉 html

一、前言

  在非靜態頁面的項目開發中,必定會涉及到對於數據庫的訪問,最開始呢,我們使用 Ado.Net,通過編寫 SQL 幫助類幫我們實現對於數據庫的快速訪問,後來,ORM(Object Relational Mapping,對象關系映射)出現了,我們開始使用 EF、Dapper、NHibernate,亦或是國人的 SqlSugar 代替我們原來的 SqlHelper.cs。通過這些 ORM 工具,我們可以很快速的將數據庫中的表與代碼中的類進行映射,同時,通過編寫 SQL 或是 Lambda 表達式的方式,更加便捷的實現對於數據層的訪問。

  就像文章標題中所說的這樣,在這個項目中我是使用的 Dapper 來進行的數據訪問,每個人都有自己的編程習慣,本篇文章只是介紹我在 Grapefruit.VuCore 這個項目中是如何基於 Dapper 創建自己使用的幫助方法的,不會涉及各種 ORM 工具的對比,請友善查看、討論。

  系列目錄地址:ASP.NET Core 項目實戰
  倉儲地址:https://github.com/Lanesra712/Grapefruit.VuCore

二、Step by Step

  1、整體思路

  在 Grapefruit.VuCore 這個項目中,我選擇將 SQL 語句存儲在 XML 文件中(XML 以嵌入的資源的方式嵌入到程序集中),通過編寫中間件的方式,在程序運行時將存儲有 SQL 語句的 XML 程序集寫入到 Redis 緩存中。當使用到 SQL 語句時,通過 Redis 中的 Key 值進行獲取到 Value,從而將 SQL 語句與我們的代碼進行拆分。

  涉及到的類文件主要是在以下的類庫中,基於 Dapper 的數據訪問代碼則位於基礎構造層(02_Infrastructure)中,而使用到這些數據訪問代碼的,有且僅在位於領域層(03_Domain)中的代碼。同時,領域層的文件分布結構和應用層(04_Applicatin)保持相同。

技術分享圖片

  2、擴展數據訪問方法

  在使用 Dapper 之前,我們首先需要在 Grapefruit.Infrastructure 這個類庫中添加對於 Dapper 的引用。同時,因為需要將 SQL 語句存儲到 Redis 緩存中,與之前使用 Redis 存儲 Token 時相同,這裏,也是使用的微軟的分布式緩存接口,因此,同樣需要添加對於此 DLL 的引用。

Install-Package Dapper
Install-Package Microsoft.Extensions.Caching.Abstractions

  在 Grapefruit.Infrastructure 類庫中創建一個 Dapper 文件夾,我們基於 Dapper 的擴展代碼全部置於此處,整個的代碼結構如下圖所示。

技術分享圖片

  在整個 Dapper 文件夾下類/接口/枚舉文件,主要可以按照功能分為三部分。

  2.1、輔助功能文件

  主要包含 DataBaseTypeEnum 這個枚舉類以及 SqlCommand 這個用來將存儲在 XML 中的 SQL 進行映射的幫助類。

  DataBaseTypeEnum 這個數據庫類型枚舉類主要定義了可以使用的數據庫類型。我們知道,Dapper 這個 ORM 主要是通過擴展 IDbConnection 接口,從而給我們提供附加的數據操作功能,而我們在創建數據庫連接對象時,不管是 SqlConnection 還是 MySqlConnection 最終對於數據庫最基礎的操作,都是繼承於 IDbConnection 這個接口。因此,我們可以在後面創建數據庫連接對象時,通過不同的枚舉值,創建針對不同數據庫操作的數據庫連接對象。

技術分享圖片
public enum DataBaseTypeEnum
{
    SqlServer = 1,
    MySql = 2,
    PostgreSql = 3,
    Oracle = 4
}
技術分享圖片

  SqlCommand 這個類文件只是定義了一些屬性,因為我是將 SQL 語句寫到 XML 文件中,同時會將 XML 文件存儲到 Redis 緩存中,因此,SqlCommand 這個類主要用來將我們獲取到的 SQL 語句與類文件做一個映射關系。

技術分享圖片
public class SqlCommand
{
    /// <summary>
    /// SQL語句名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// SQL語句或存儲過程內容
    /// </summary>
    public string Sql { get; set; }
}
技術分享圖片

  2.2、SQL 存儲讀取

  對於 SQL 語句的存儲、讀取,我定義了一個 IDataRepository 接口,DataRepository 繼承於 IDataRepository 實現對於 SQL 語句的操作。

技術分享圖片
public interface IDataRepository
{
    /// <summary>
    /// 獲取 SQL 語句
    /// </summary>
    /// <param name="commandName"></param>
    /// <returns></returns>
    string GetCommandSQL(string commandName);

    /// <summary>
    /// 批量寫入 SQL 語句
    /// </summary>
    void LoadDataXmlStore();
}
技術分享圖片

  存儲 SQL 的 XML 我是以附加的資源存儲到 dll 中,因此,這裏我是通過加載 dll 的方式獲取到所有的 SQL 語句,之後,根據 Name 屬性判斷 Redis 中是否存在,當不存在時就寫入 Redis 緩存中。核心的代碼如下所示,如果你需要查看完整的代碼,可以去 Github 上查看。

技術分享圖片
/// <summary>
/// 載入dll中包含的SQL語句
/// </summary>
/// <param name="fullPath">命令名稱</param>
private void LoadCommandXml(string fullPath)
{
    SqlCommand command = null;
    Assembly dll = Assembly.LoadFile(fullPath);
    string[] xmlFiles = dll.GetManifestResourceNames();
    for (int i = 0; i < xmlFiles.Length; i++)
    {
        Stream stream = dll.GetManifestResourceStream(xmlFiles[i]);
        XElement rootNode = XElement.Load(stream);
        var targetNodes = from n in rootNode.Descendants("Command")
                          select n;
        foreach (var item in targetNodes)
        {
            command = new SqlCommand
            {
                Name = item.Attribute("Name").Value.ToString(),
                Sql = item.Value.ToString().Replace("<![CDATA[", "").Replace("]]>", "")
            };
            command.Sql = command.Sql.Replace("\r\n", "").Replace("\n", "").Trim();
            LoadSQL(command.Name, command.Sql);
        }
    }
}

/// <summary>
/// 載入SQL語句
/// </summary>
/// <param name="commandName">SQL語句名稱</param>
/// <param name="commandSQL">SQL語句內容</param>
private void LoadSQL(string commandName, string commandSQL)
{
    if (string.IsNullOrEmpty(commandName))
    {
        throw new ArgumentNullException("CommandName is null or empty!");
    }

    string result = GetCommandSQL(commandName);

    if (string.IsNullOrEmpty(result))
    {
        StoreToCache(commandName, commandSQL);
    }
}
技術分享圖片

  2.3、數據操作

  對於數據的操作,這裏我定義了 IDataAccess 這個接口,提供了同步、異步的方式,實現對於數據的訪問。在項目開發中,對於數據的操作,更多的還是根據字段值獲取對象、獲取對象集合、執行 SQL 獲取受影響的行數,獲取字段值,所以,這裏主要就定義了這幾類的方法。

技術分享圖片
public interface IDataAccess
{
    /// 關閉數據庫連接
    bool CloseConnection(IDbConnection connection);

    /// 數據庫連接
    IDbConnection DbConnection();

    /// 執行SQL語句或存儲過程返回對象
    T Execute<T>(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL語句返回對象
    T Execute<T>(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程返回對象
    Task<T> ExecuteAsync<T>(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL語句返回對象
    Task<T> ExecuteAsync<T>(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程,返回IList<T>對象
    IList<T> ExecuteIList<T>(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程,返回IList<T>對象
    IList<T> ExecuteIList<T>(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程,返回IList<T>對象
    Task<IList<T>> ExecuteIListAsync<T>(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程,返回IList<T>對象
    Task<IList<T>> ExecuteIListAsync<T>(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程返回受影響行數
    int ExecuteNonQuery(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程返回受影響行數
    int ExecuteNonQuery(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程返回受影響行數
    Task<int> ExecuteNonQueryAsync(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL語句或存儲過程返回受影響行數
    Task<int> ExecuteNonQueryAsync(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行語句返回T對象
    T ExecuteScalar<T>(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行語句返回T對象
    Task<T> ExecuteScalarAsync<T>(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);
}
技術分享圖片

  在 IDataAccess 接口的功能實現與調用上,我采用了代理模式的方式,會涉及到 DataAccess、DataAccessProxy、DataAccessProxyFactory、DBManager 這四個類文件,之間的調用過程如下。

  DataAccess 是接口的實現類,通過下面的幾個類進行隱藏,不直接暴露給外界方法。一些接口的實現如下所示。

技術分享圖片
/// <summary>
/// 創建數據庫連接
/// </summary>
/// <returns></returns>
public IDbConnection DbConnection()
{
    IDbConnection connection = null;
    switch (_dataBaseType)
    {
        case DataBaseTypeEnum.SqlServer:
            connection = new SqlConnection(_connectionString);
            break;
        case DataBaseTypeEnum.MySql:
            connection = new MySqlConnection(_connectionString);
            break;
    };
    return connection;
}

/// <summary>
/// 執行SQL語句或存儲過程,返回IList<T>對象
/// </summary>
/// <typeparam name="T">類型</typeparam>
/// <param name="sql">SQL語句 or 存儲過程名</param>
/// <param name="param">參數</param>
/// <param name="transaction">外部事務</param>
/// <param name="connection">數據庫連接</param>
/// <param name="commandType">命令類型</param>
/// <returns></returns>
public IList<T> ExecuteIList<T>(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text)
{
    IList<T> list = null;
    if (connection.State == ConnectionState.Closed)
    {
        connection.Open();
    }
    try
    {
        if (commandType == CommandType.Text)
        {
            list = connection.Query<T>(sql, param, transaction, true, null, CommandType.Text).ToList();
        }
        else
        {
            list = connection.Query<T>(sql, param, transaction, true, null, CommandType.StoredProcedure).ToList();
        }
    }
    catch (Exception ex)
    {
        _logger.LogError($"SQL語句:{sql},使用外部事務執行 ExecuteIList<T> 方法出錯,錯誤信息:{ex.Message}");
        throw ex;
    }
    return list;
}
技術分享圖片

  DBManager 是外界方法訪問的類,通過 CreateDataAccess 方法會創建一個 IDataAccess 對象,從而達到訪問接口中方法的目的。

技術分享圖片
[ThreadStatic]
private static IDataAccess _sMsSqlFactory;

/// <summary>
/// 
/// </summary>
/// <param name="cp"></param>
/// <returns></returns>
private static IDataAccess CreateDataAccess(ConnectionParameter cp)
{
    return new DataAccessProxy(DataAccessProxyFactory.Create(cp));
}

/// <summary>
/// MsSQL 數據庫連接字符串
/// </summary>
public static IDataAccess MsSQL
{
    get
    {
        ConnectionParameter cp;
        if (_sMsSqlFactory == null)
        {
            cp = new ConnectionParameter
            {
                ConnectionString = ConfigurationManager.GetConfig("ConnectionStrings:MsSQLConnection"),
                DataBaseType = DataBaseTypeEnum.SqlServer
            };
            _sMsSqlFactory = CreateDataAccess(cp);
        }
        return _sMsSqlFactory;
    }
}
技術分享圖片

  DataAccessProxy 就是實際接口功能實現類的代理,通過有參構造函數的方式進行調用,同時,類中繼承於 IDataAccess 的方法都是不實現的,都是通過 _dataAccess 調用接口中的方法。

技術分享圖片
/// <summary>
/// 
/// </summary>
private readonly IDataAccess _dataAccess;

/// <summary>
/// ctor
/// </summary>
/// <param name="dataAccess"></param>
public DataAccessProxy(IDataAccess dataAccess)
{
    _dataAccess = dataAccess ?? throw new ArgumentNullException("dataAccess is null");
}

/// <summary>
/// 執行SQL語句或存儲過程,返回IList<T>對象
/// </summary>
/// <typeparam name="T">類型</typeparam>
/// <param name="sql">SQL語句 or 存儲過程名</param>
/// <param name="param">參數</param>
/// <param name="transaction">外部事務</param>
/// <param name="connection">數據庫連接</param>
/// <param name="commandType">命令類型</param>
/// <returns></returns>
public IList<T> ExecuteIList<T>(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text)
{
    return _dataAccess.ExecuteIList<T>(sql, param, transaction, connection, commandType);
}
技術分享圖片

  DataAccessProxyFactory 這個類有一個 Create 靜態方法,通過實例化 DataAccess 類的方式返回 IDataAccess 接口,從而達到真正調用到接口實現類。

技術分享圖片
/// <summary>
/// 創建數據庫連接字符串
/// </summary>
/// <param name="cp"></param>
/// <returns></returns>
public static IDataAccess Create(ConnectionParameter cp)
{
    if (string.IsNullOrEmpty(cp.ConnectionString))
    {
        throw new ArgumentNullException("ConnectionString is null or empty!");
    }
    return new DataAccess(cp.ConnectionString, cp.DataBaseType);
}
技術分享圖片

  3、使用方法

  因為我們對於 SQL 語句的獲取全部是從緩存中獲取的,因此,我們需要在程序執行前將所有的 SQL 語句寫入 Redis 中。在 ASP.NET MVC 中,我們可以在 Application_Start 方法中進行調用,但是在 ASP.NET Core 中,我一直沒找到如何實現僅在程序開始運行時執行代碼,所以,這裏,我采用了中間件的形式將 SQL 語句存儲到 Redis 中,當然,你的每一次請求,都會調用到這個中間件。如果大家有好的方法,歡迎在評論區裏指出。

技術分享圖片
public class DapperMiddleware
{
    private readonly ILogger _logger;

    private readonly IDataRepository _repository;

    private readonly RequestDelegate _request;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="repository"></param>
    /// <param name="logger"></param>
    /// <param name="request"></param>
    public DapperMiddleware(IDataRepository repository, ILogger<DapperMiddleware> logger, RequestDelegate request)
    {
        _repository = repository;
        _logger = logger;
        _request = request;
    }

    /// <summary>
    /// 註入中間件到HttpContext中
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task InvokeAsync(HttpContext context)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        //加載存儲xml的dll
        _repository.LoadDataXmlStore();

        sw.Stop();
        TimeSpan ts = sw.Elapsed;

        _logger.LogInformation($"加載存儲 XML 文件DLL,總共用時:{ts.TotalMinutes} 秒");

        await _request(context);
    }
}
技術分享圖片

  中間件的實現,只是調用了之前定義的 IDataRepository 接口中的 LoadDataXmlStore 方法,同時記錄下了加載的時間。在 DapperMiddlewareExtensions 這個靜態類中,定義了中間件的使用方法,之後我們在 Startup 的 Configure 方法裏調用即可。

技術分享圖片
public static class DapperMiddlewareExtensions
{
    /// <summary>
    /// 調用中間件
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseDapper(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<DapperMiddleware>();
    }
}
技術分享圖片

  中間件的調用代碼如下,同時,因為我們在中間件中通過依賴註入的方式使用到了 IDataRepository 接口,所以,我們也需要在 ConfigureServices 中註入該接口,這裏,采用單例的方式即可。

技術分享圖片
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        //DI Sql Data
        services.AddTransient<IDataRepository, DataRepository>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider)
    {
        //Load Sql Data
        app.UseDapper();
    }
}
技術分享圖片

   當所有的 SQL 語句寫入到緩存中後,我們就可以使用了,這裏的示例代碼實現的是上一篇(ASP.NET Core 實戰:基於 Jwt Token 的權限控制全揭露)中,進行 Jwt Token 授權,驗證登錄用戶信息的功能。

技術分享圖片

  整個的調用過程如下圖所示。

技術分享圖片

  在 SecretDomain 中,我定義了一個 GetUserForLoginAsync 方法,通過帳戶名和密碼獲取用戶的信息,調用了之前定義的數據訪問方法。

技術分享圖片
public class SecretDomain : ISecretDomain
{
    #region Initialize

    /// <summary>
    /// 倉儲接口
    /// </summary>
    private readonly IDataRepository _repository;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="repository"></param>
    public SecretDomain(IDataRepository repository)
    {
        _repository = repository;
    }

    #endregion

    #region API Implements

    /// <summary>
    /// 根據帳戶名、密碼獲取用戶實體信息
    /// </summary>
    /// <param name="account">賬戶名</param>
    /// <param name="password">密碼</param>
    /// <returns></returns>
    public async Task<IdentityUser> GetUserForLoginAsync(string account, string password)
    {
        StringBuilder strSql = new StringBuilder();
        strSql.Append(_repository.GetCommandSQL("Secret_GetUserByLoginAsync"));
        string sql = strSql.ToString();

        return await DBManager.MsSQL.ExecuteAsync<IdentityUser>(sql, new
        {
            account,
            password
        });
    }

    #endregion
}
技術分享圖片

  XML 的結構如下所示,註意,這裏需要修改 XML 的屬性,生成操作改為附加的資源。

技術分享圖片
<?xml version="1.0" encoding="utf-8" ?>
<Commands>
  <Command Name="Secret_GetUserByLoginAsync">
    <![CDATA[
        SELECT Id ,Name ,Account ,Password ,Salt
          FROM IdentityUser
          WHERE Account=@account AND Password=@password;
      ]]>
  </Command>
  <Command Name="Secret_NewId">
    <![CDATA[
        select NEWID();
      ]]>
  </Command>
</Commands>
技術分享圖片

技術分享圖片

  因為篇幅原因,這裏就不把所有的代碼都列出來,整個調用的過程演示如下,如果有不明白的,或是有什麽好的建議的,歡迎在評論區中提出。因為,數據庫表並沒有設計好,這裏只是建了一個實驗用的表,,這裏我使用的是 SQL Server 2012,創建表的 SQL 語句如下。

技術分享圖片
USE [GrapefruitVuCore]
GO

ALTER TABLE [dbo].[IdentityUser] DROP CONSTRAINT [DF_User_Id]
GO

/****** Object:  Table [dbo].[IdentityUser]    Script Date: 2019/2/24 9:41:15 ******/
DROP TABLE [dbo].[IdentityUser]
GO

/****** Object:  Table [dbo].[IdentityUser]    Script Date: 2019/2/24 9:41:15 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[IdentityUser](
    [Id] [uniqueidentifier] NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [Account] [nvarchar](50) NOT NULL,
    [Password] [nvarchar](100) NOT NULL,
    [Salt] [uniqueidentifier] NOT NULL,
 CONSTRAINT [PK__User__3214EC07D257C709] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[IdentityUser] ADD  CONSTRAINT [DF_User_Id]  DEFAULT (newid()) FOR [Id]
GO
技術分享圖片

技術分享圖片

三、總結

   這一章主要是介紹下我是如何使用 Dapper 構建我的數據訪問幫助方法的,每個人都會有自己的編程習慣,這裏只是給大家提供一個思路,適不適合你就不一定啦。因為年後工作開始變得多起來了,現在主要都是周末才能寫博客了,所以更新的速度會變慢些,同時,這一系列的文章,按我的設想,其實還有一兩篇文章差不多就結束了(VUE 前後端交互、Docker 部署),嗯,因為 Vue 那塊我還在學習中(其實就是很長時間沒看了。。。),所以接下來的一段時間可能會側重於 Vue 系列(Vue.js 牛刀小試),ASP.NET Core 系列可能會不定期更新,希望大家同樣可以多多關註啊。最後,感謝之前贊賞的小夥伴。

NET Core 實戰 Dapper 擴展數據訪問