歡迎光臨
每天分享高質量文章

ASP.NET Core 實戰:基於 Dapper 擴充套件你的資料訪問方法

一、前言

  在非靜態頁面的專案開發中,必定會涉及到對於資料庫的訪問,最開始呢,我們使用 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 這個介面。因此,我們可以在後面建立資料庫連線物件時,透過不同的列舉值,建立針對不同資料庫操作的資料庫連線物件。

  SqlCommand 這個類檔案只是定義了一些屬性,因為我是將 SQL 陳述句寫到 XML 檔案中,同時會將 XML 檔案儲存到 Redis 快取中,因此,SqlCommand 這個類主要用來將我們獲取到的 SQL 陳述句與類檔案做一個對映關係。

  2.2、SQL 儲存讀取

  對於 SQL 陳述句的儲存、讀取,我定義了一個 IDataRepository 介面,DataRepository 繼承於 IDataRepository 實現對於 SQL 陳述句的操作。

public interface IDataRepository
{
    /// 
    /// 獲取 SQL 陳述句
    ///

///
///
string GetCommandSQL(string commandName);

///
/// 批次寫入 SQL 陳述句
///
void LoadDataXmlStore();
}

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

/// 
/// 載入dll中包含的SQL陳述句
///

/// 命令名稱
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(, “”).Replace(]]>, “”)
};
command.Sql
= command.Sql.Replace(\r\n, “”).Replace(\n, “”).Trim();
LoadSQL(command.Name, command.Sql);
}
}
}

///
/// 載入SQL陳述句
///
/// SQL陳述句名稱
/// SQL陳述句內容
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(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句傳回物件
    T Execute(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句或儲存過程傳回物件
    Task ExecuteAsync(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句傳回物件
    Task ExecuteAsync(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句或儲存過程,傳回IList物件
    IList ExecuteIList(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句或儲存過程,傳回IList物件
    IList ExecuteIList(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句或儲存過程,傳回IList物件
    Task> ExecuteIListAsync(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行SQL陳述句或儲存過程,傳回IList物件
    Task> ExecuteIListAsync(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(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);

    /// 執行陳述句傳回T物件
    Task ExecuteScalarAsync(string sql, object param, bool hasTransaction = false, CommandType commandType = CommandType.Text);
}

  在 IDataAccess 介面的功能實現與呼叫上,我採用了代理樣式的方式,會涉及到 DataAccess、DataAccessProxy、DataAccessProxyFactory、DBManager 這四個類檔案,之間的呼叫過程如下。

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

/// 
/// 建立資料庫連線
///

///
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;
}

///
/// 執行SQL陳述句或儲存過程,傳回IList物件
///
/// 型別
/// SQL陳述句 or 儲存過程名
/// 引數
/// 外部事務
/// 資料庫連線
/// 命令型別
///
public IList ExecuteIList(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text)
{
IList
list = null;
if (connection.State == ConnectionState.Closed)
{
connection.Open();
}
try
{
if (commandType == CommandType.Text)
{
list
= connection.Query(sql, param, transaction, true, null, CommandType.Text).ToList();
}
else
{
list
= connection.Query(sql, param, transaction, true, null, CommandType.StoredProcedure).ToList();
}
}
catch (Exception ex)
{
_logger.LogError($
SQL陳述句:{sql},使用外部事務執行 ExecuteIList 方法出錯,錯誤資訊:{ex.Message});
throw ex;
}
return list;
}

  DBManager 是外界方法訪問的類,透過 CreateDataAccess 方法會建立一個 IDataAccess 物件,從而達到訪問介面中方法的目的。

[ThreadStatic]
private static IDataAccess _sMsSqlFactory;

/// 
/// 
///

///
///
private static IDataAccess CreateDataAccess(ConnectionParameter cp)
{
return new DataAccessProxy(DataAccessProxyFactory.Create(cp));
}

///
/// MsSQL 資料庫連線字串
///
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 呼叫介面中的方法。

/// 
/// 
///

private readonly IDataAccess _dataAccess;

///
/// ctor
///
///
public DataAccessProxy(IDataAccess dataAccess)
{
_dataAccess
= dataAccess ?? throw new ArgumentNullException(dataAccess is null);
}

///
/// 執行SQL陳述句或儲存過程,傳回IList物件
///
/// 型別
/// SQL陳述句 or 儲存過程名
/// 引數
/// 外部事務
/// 資料庫連線
/// 命令型別
///
public IList ExecuteIList(string sql, object param, IDbTransaction transaction, IDbConnection connection, CommandType commandType = CommandType.Text)
{
return _dataAccess.ExecuteIList(sql, param, transaction, connection, commandType);
}

  DataAccessProxyFactory 這個類有一個 Create 靜態方法,透過實體化 DataAccess 類的方式傳回 IDataAccess 介面,從而達到真正呼叫到介面實現類。

/// 
/// 建立資料庫連線字串
///

///
///
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;

    /// 
    /// ctor
    ///

///
///
///
public DapperMiddleware(IDataRepository repository, ILogger logger, RequestDelegate request)
{
_repository
= repository;
_logger
= logger;
_request
= request;
}

///
/// 註入中介軟體到HttpContext中
///
///
///
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
{
    /// 
    /// 呼叫中介軟體
    ///

///
///
public static IApplicationBuilder UseDapper(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}

  中介軟體的呼叫程式碼如下,同時,因為我們在中介軟體中透過依賴註入的方式使用到了 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();
    }

    // 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

    /// 
    /// 倉儲介面
    ///

private readonly IDataRepository _repository;

///
/// ctor
///
///
public SecretDomain(IDataRepository repository)
{
_repository
= repository;
}

#endregion

#region API Implements

///
/// 根據帳戶名、密碼獲取使用者物體資訊
///
/// 賬戶名
/// 密碼
///
public async Task GetUserForLoginAsync(string account, string password)
{
StringBuilder strSql
= new StringBuilder();
strSql.Append(_repository.GetCommandSQL(
Secret_GetUserByLoginAsync));
string sql = strSql.ToString();

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

#endregion
}

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

xml version="1.0" encoding="utf-8" ?>
<Commands>
  <Command Name="Secret_GetUserByLoginAsync">
    
        SELECT Id ,Name ,Account ,Password ,Salt
          FROM IdentityUser
          WHERE Account=@account AND Password=@password;
      ]]>
  Command>
  <Command Name="Secret_NewId">
    
        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 系列可能會不定期更新,希望大家同樣可以多多關註啊。最後,感謝之前贊賞的小夥伴。

作者:墨墨墨墨小宇

出處:https://www.cnblogs.com/danvic712/p/10425501.html

本站使用「署名 4.0 國際」創作共享協議,轉載請在文章明顯位置註明作者及出處。

個人簡介:96年生人,出生於安徽某四線城市,畢業於Top 10000000 院校。.NET 程式員,槍手死忠,喵星人。於2016年12月開始.NET程式員生涯,微軟.NET技術的堅定堅持者,立志成為雲養貓的少年中面向谷歌程式設計最厲害的.NET程式員。

個人部落格:https://yuiter.com

原文地址:https://www.cnblogs.com/danvic712/p/10425501.html

贊(0)

分享創造快樂