在非靜態頁面的項目開發中,一定會涉及到對於數據庫的訪問,最開始呢,咱們使用 Ado.Net,經過編寫 SQL 幫助類幫咱們實現對於數據庫的快速訪問,後來,ORM(Object Relational Mapping,對象關係映射)出現了,咱們開始使用 EF、Dapper、NHibernate,亦或是國人的 SqlSugar 代替咱們原來的 SqlHelper.cs。經過這些 ORM 工具,咱們能夠很快速的將數據庫中的表與代碼中的類進行映射,同時,經過編寫 SQL 或是 Lambda 表達式的方式,更加便捷的實現對於數據層的訪問。html
就像文章標題中所說的這樣,在這個項目中我是使用的 Dapper 來進行的數據訪問,每一個人都有本身的編程習慣,本篇文章只是介紹我在 Grapefruit.VuCore 這個項目中是如何基於 Dapper 建立本身使用的幫助方法的,不會涉及各類 ORM 工具的對比,請友善查看、討論。git
系列目錄地址:ASP.NET Core 項目實戰
倉儲地址:https://github.com/Lanesra712/Grapefruit.VuCoregithub
在 Grapefruit.VuCore 這個項目中,我選擇將 SQL 語句存儲在 XML 文件中(XML 以嵌入的資源的方式嵌入到程序集中),經過編寫中間件的方式,在程序運行時將存儲有 SQL 語句的 XML 程序集寫入到 Redis 緩存中。當使用到 SQL 語句時,經過 Redis 中的 Key 值進行獲取到 Value,從而將 SQL 語句與咱們的代碼進行拆分。sql
涉及到的類文件主要是在如下的類庫中,基於 Dapper 的數據訪問代碼則位於基礎構造層(02_Infrastructure)中,而使用到這些數據訪問代碼的,有且僅在位於領域層(03_Domain)中的代碼。同時,領域層的文件分佈結構和應用層(04_Applicatin)保持相同。數據庫
在使用 Dapper 以前,咱們首先須要在 Grapefruit.Infrastructure 這個類庫中添加對於 Dapper 的引用。同時,由於須要將 SQL 語句存儲到 Redis 緩存中,與以前使用 Redis 存儲 Token 時相同,這裏,也是使用的微軟的分佈式緩存接口,所以,一樣須要添加對於此 DLL 的引用。編程
Install-Package Dapper Install-Package Microsoft.Extensions.Caching.Abstractions
在 Grapefruit.Infrastructure 類庫中建立一個 Dapper 文件夾,咱們基於 Dapper 的擴展代碼所有置於此處,整個的代碼結構以下圖所示。後端
在整個 Dapper 文件夾下類/接口/枚舉文件,主要能夠按照功能分爲三部分。緩存
2.一、輔助功能文件app
主要包含 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.二、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.三、數據操做
對於數據的操做,這裏我定義了 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); }
由於咱們對於 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 系列可能會不按期更新,但願你們一樣能夠多多關注啊。最後,感謝以前讚揚的小夥伴。