1.NetDh架構開始的需求場景
需求場景:
1.之前公司有不同.net項目組,有的項目是用SqlServer做資料庫,有的項目是用Oracle,後面也有可能會用到Mysql等,而且要考慮後續擴充成主從庫、多庫的需求。(其實不管有沒有這個需求,Dapper的封裝應當像NetDh架構裡封裝的那樣使用);
2.涉及日志操作類的設計,需要記錄使用者記錄檔、記錄系統異常日志等;
3.涉及緩存操作類的設計,這點不用需求都應該當考慮,不管是小項目的記憶體緩存還是大項目中的Redis/Memcache等;
4.涉及二次開發模式簡單的設計。因為多個客戶需要同一個項目産品,但是客戶之間對該産品的需求點又有些不一樣。
本文先講為了第1點需求而封裝的資料庫操作類,其它三點在接下來文章中也會介紹。
2.ORM架構Dapper介紹
Dapper是輕量級高效的架構,它高效原因是利用Emit技術+各種解析緩存+DataReader。
Dapper可以在所有Ado.net Providers下工作,包括SQL Server, Oracle, MySQL , SQLite, PostgreSQL, sqlce, firebird 等,這些資料庫操作類都有實作IDbConnection接口。你看源碼會發現,Dapper提供的public方法大都是對IDbConnection的擴充。
DapperExtensions是Dapper的第三方插件之一(NetDh架構是用Dapper+DapperExtensions的組合),Dapper常用的代碼是 Query<T>(selectSql..)把資料庫擷取的記錄轉成實體類對象,而DapperExtensions是封裝了Dapper,支援諸如 Get<T>(id)、Insert<T>、Update<T>等函數,可以讓你不寫sql就能簡單操作資料庫資料。
3.資料庫操作類-在Dapper+DapperExtensions基礎上封裝
3.1.總體說明

如上圖,NetDh架構是把Dapper(.net4.5.1)目前最新源碼和DapperExtensions源碼合并在同一個程式集,然後添加到解決方案。有源碼就可以随便調試,深入了解Dapper和學習Dapper。
在NetDh.DbUtility程式集中,DbHandleBase是個抽象基類,它封裝了“資料庫常用的操作函數”,如下圖:
DbHandleBase基類封裝了Dapper+DapperExtensions,如果要實作SqlServerHandle操作類,那麼隻要繼承DbHandleBase,然後重寫基類的CreateConnection抽象函數(為了擷取連接配接對象),即可擁有這些“資料庫常用的操作函數”。其它資料庫類型也一樣,很簡單吧。到底有多麼簡單,我們看下SqlServerHandle類,如下圖
一個override CreateConnection搞定,每個資料庫的連接配接對象不一樣,是以這個是必要的override。
說明,在NetDh代碼中,Oracle操作類不使用微軟早期的OracleClient(微軟已經不維護),而是使用Oracle官方ODP.NET(nuget下載下傳 Oracle.ManagedDataAccess.dll),不用再安裝OraInsClient和配置tnsnames.ora。
另外,用了Dapper(ORM)一般就是不用ExecuteDataTable和ExecuteDataset,為什麼DbHandleBase還開放出來,原因:
(1)winform項目中的Grid經常要用到DataTable的DataView;
(2)sql的參數統一簡單寫法,由dapper處理;
(3)Dapper做了各種解析緩存。
貼個DbHandleBase中的ExecuteDataTable的代碼及其注釋:
/*
* 說明:winform中經常會用到DataTable的DataView,友善Grid展示與過濾,是以開放ExecuteDataTable和ExecuteDataset;
* 如果是B/S系統,建議用以上的Query系列函數。
*/
/// <summary>
/// 執行sql語句,并傳回DataTable(适用于Dapper支援的所有資料庫類型)
/// </summary>
/// <param name="sql">sql語句</param>
/// <param name="param">匿名對象的參數,可以簡單寫,比如 new {Name="user1"} 即可,會統一處理參數和參數的size</param>
public virtual DataTable ExecuteDataTable(string sql, dynamic param = null, int? cmdTimeout = DEFAULTTIMEOUT, IDbTransaction tran = null, CommandType? cmdType = null)
{
sql = CheckSql(sql);
var conn = CreateConnectionAndOpen();
try
{
//這邊用Dapper的ExecuteReader,統一了函數參數寫法,不用使用SqlParameter。
using (var reader = conn.ExecuteReader(sql, (object)param, tran, cmdTimeout, cmdType))
{
var dataTable = DataReaderToDataTable(reader);
return dataTable;
}
}
finally
{
conn.CloseIfOpen();
}
}
3.2.調用資料庫操作類的示例代碼
以SqlServer資料庫為例,以下直接上代碼,代碼中的注釋很詳細也很有幫助。
值得一提的是:當取數條件比較複雜或者需要關聯多表時,許多人還是不寫sql而是喜歡用ORM的Linq表達式。建議簡單的單表CRUD操作不用寫sql,而比較複雜的業務邏輯建議是寫sql,一是sql文法簡單明了通用,每批技術員都看得懂;二是你可以對複雜的業務邏輯明确執行什麼用的sql語句,怎麼樣的執行計劃。如果你Linq寫得複雜,都不知道ORM會給你生成什麼樣的sql出來。
/// <summary>
/// NetDh子產品使用示例代碼
/// </summary>
public class NetDhExample
{
#region 用全局靜态變量實作單例。
/// <summary>
/// 服務端使用資料庫操作對象,前端不可直接使用
/// </summary>
public static DbHandleBase DbHandle { get; set; }
//說明:如果你有多庫,比如讀寫分離中的隻讀庫,則再定義一個資料庫操作對象即可。
public static DbHandleBase ReadDbHandle { get; set; }
#endregion
/// <summary>
/// 靜态構造函數,隻會初始化一次
/// </summary>
static NetDhExample()
{
//初始化資料庫操作對象
DbHandle = new SqlServerHandle(connStr);
//如果有多庫,可再new個對象
//ReadDbHandle = new SqlServerHandle(connStrForRead);
}
/// <summary>
/// 子產品使用的示例代碼
/// </summary>
public static void TestMain()
{
#region 資料庫互動(sqlserver+Dapper+DapperExtension)
//---------CRUD操作--------
//實體類中的第一個Id或者以Id結尾的字段,會被預設當作主鍵,Dapper不僅建議你的表主鍵為Id或以Id結尾的字段,
//而且Dapper預設主鍵字段在資料庫表裡有預設值(比如有設定為自增長),關于為什麼建議用自增長主鍵,可以翻一下我之前的部落格文章《SQL Server索引原了解析》。
//如果表中的主鍵不符合此規定(比如表主鍵是MainKey字段),則需要自定義Map映射,參考以下的“DapperExtensions進階”
var user = DbHandle.Get<TbUser>(1);//這邊1産生的是 where Id=1 的條件
//Get<TbUser>是DapperExtensions的功能,不是Dapper的功能。
//Get<TbUser>這種寫法類似select *,并不是好作法,但是它寫法友善,隻取一筆影響不大。一般是select你要的字段,而不是select所有字段。具體問題具體分析。
user.Name = "new name";
DbHandle.Update(user);
/* 注意如果用實體類去update,就算隻更新一個字段,DapperExtension也會生成除了id主鍵之外的所有字段的更新。
* 多餘的更新會增加資料庫開銷,尤其有非聚集索引字段。是以,建議如果要用此Update函數,則隻用于基礎表。
*/
var lastInsertId = DbHandle.Insert(user);//傳回lastInsertId。因為它生成的語句包含:SELECT CAST(SCOPE_IDENTITY() AS BIGINT) AS [Id]
/* DbHandle.Insert(user);是不會報主鍵重複。以下是DbHandle.Insert産生的文法(來自SQL Server Profiler工具),
* 不會生成主鍵Id的Insert。因為Dapper預設你的主鍵如果是整形則是KeyType.Identity類型(即預設主鍵字段在資料庫表裡有預設值,比如有設定為自增長),
* DbHandle.Insert(user) DapperExtensions産生的sql語句:
exec sp_executesql N'INSERT INTO [TbUser] ([TbUser].[Name], [TbUser].[Age], [TbUser].[Remark], [TbUser].[Department], [TbUser].[CreateTime]) VALUES (@Name, @Age, @Remark, @Department, @CreateTime);
SELECT CAST(SCOPE_IDENTITY() AS BIGINT) AS [Id]',N'@CreateTime datetime,@Department nvarchar(200),@Age decimal(6,4),@Name nvarchar(200),@Remark nvarchar(4000)',@CreateTime='2018-06-07 20:05:33.630',@Department=N'D1',@Age=30,@Name=N'new name',@Remark=N'remark1'
*/
user.Id = 1001;
DbHandle.Delete(user);
//DbHandle.Delete(user);隻和主鍵Id有關。産生的sql:exec sp_executesql N'DELETE FROM [TbUser] WHERE ([TbUser].[Id] = @Id_0)',N'@Id_0 int',@Id_0=1001
DbHandle.Delete(new TbUser() { Id = 1001 });
//---------------------
//1.使用DapperExtensions過濾條件取Id<100的TbUser降序資料清單
var filter = Predicates.Field<TbUser>(f => f.Id, Operator.Lt, 100);
var sort = new List<ISort> { Predicates.Sort<TbUser>(f => f.Id, false) };//false降序
var users2 = DbHandle.GetList<TbUser>(filter, sort);
//如果需要多個過濾條件需要用到PredicatesGroup嵌套,這是DapperExtensions的功能(不是Dapper原生功能)。
//複雜的sql建議用直接寫sql(如下簡潔版),直接寫複雜sql的優點:select字段可選、sql執行計劃可控。
//2.使用sql取Id<100的TbUser降序資料清單(簡潔版)可以指定隻取你要的字段Id,Name。簡單明了通用。
var uses = DbHandle.Query<TbUser>("select Id,Name from TbUser where Id<@maxId order by Id desc", new { maxId = 100 });
//winform中經常會用到DataTable的DataView,友善Grid展示與過濾,是以開放ExecuteDataTable和ExecuteDataset
var table = DbHandle.ExecuteDataTable("select Id,Name from TbUser where Id<100 order by Id desc");
//---------分頁--------
//1.單表分頁(第3頁,一頁10筆) DapperExtension支援單表
var pageUsers = DbHandle.GetPageByModel<TbUser>(null, sort, 2, 10);//參數startPageIndex第1頁是從0開始
//2.sqlserver 自己sql分頁(第3頁,一頁10筆),并且擷取表記錄總數
var pageSql = @" select top 10 * from(
select (row_number() over(order by Id))as rowId,* from TbUser where Id<@maxId) as a where a.rowId >20 order by a.rowId;
select count(1) from TbUser";
var dataset = DbHandle.ExecuteDataSet(pageSql, new { maxId = 1000 });
//3.封裝的sql分頁,為了支援不同資料庫分頁寫法不同(第3頁,一頁10筆),并且擷取表記錄總數,适合較複雜的分頁
var pageSql1 = DbHandle.GetPageSql("select * from TbUser A where A.Id<@maxId", "order by A.Id", 2, 10, "select count(1) from TbUser");//參數startPageIndex第1頁是從0開始
dataset = DbHandle.ExecuteDataSet(pageSql1, new { maxId = 1000 });
//---------多表關聯--------
//select的字段并沒有對應的實體類時,可用QueryDynamics。DbHandle也支援傳回IEnumerable<Hashtable>的QueryHashtables,友善轉為json格式
var dyObj = DbHandle.QueryDynamics("select A.Name,B.Amount from TbUser A inner join TbOrder B on B.Uid=A.Id where A.Id=@Id", new { Id = 10 });
//---------使用存儲過程--------
//執行存儲過程就是把函數參數CommandType設定為CommandType.StoredProcedure即可,存儲過程的參數傳遞直接 new {@p=value}
#endregion
#region DapperExtensions進階--自定義Map映射。
/*項目起初,規範好表設計,一般是不會用到自定義Map映射。如果是現有項目,可酌情考慮*/
//自定義Map,具體參考TbUser.cs裡的代碼說明
//以下這句是初始化,告訴DapperExtensions
DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(TbUserMapper).Assembly });
//DapperExtensions.DapperExtensions.SqlDialect = new DapperExtensions.Sql.SqlServerDialect();//DapperExtensions預設就是SqlServerDialect
#endregion
}
}
3.3.DapperExtensions進階--自定義Map映射
比如你的實體類名是TbUser,而對應的資料庫表名是UserTable,或者實體類的某個屬性名和資料庫表字段名不一樣,則需要Map映射,映射支援以下幾種情況,看代碼和注釋:
#region 如果需要自定義Map映射,可參考:
public class TbUserMapper : ClassMapper<TbUser>
{
public TbUserMapper()
{
//1.use different table name
Table("UserTable");//把實體類的資料庫表名指定為UserTable
//2.use a custom schema
//Schema("not_dbo_schema");
//3.have a custom primary key
//KeyType.Assigned說明主鍵在資料庫表無預設值(比如是非自增長的主鍵)
//KeyType.Identity說明主鍵在資料庫表有預設值(比如是自增長的主鍵)
//Map(x => x.MainKey).Key(KeyType.Assigned);
//4.Use a different name property from database column
//Map(x => x.Remark).Column("Bar");//把實體類的Remark屬性指定為資料庫表Bar字段
//5.Ignore this property entirely
//Map(x => x.SecretDataMan).Ignore();//忽略實體類中的SecretDataMan字段,即它不是資料庫表字段。
//optional, map all other columns
AutoMap();
}
//啟動程式時,執行以下定義:
//DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(TbUserMapper).Assembly });
//當你有很多個Model類都有自定義Map時,而且這些自定Map都在同一個程式集,那麼隻要上面那一句就可以了。它會檢索整個Assemble去查找出所有繼承ClassMapper的類。
}
#endregion
怎麼讓你自定義的映射生效呢,上面代碼最後一段就是:
//啟動程式時,執行以下定義:
DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(TbUserMapper).Assembly });
3.4.源碼
國外有github,國内有碼雲,在國内使用碼雲速度非常快。NetDh架構源碼放在碼雲上:
https://gitee.com/donghan/NetDh-Framework
分享、互相交流學習