需求场景:前端
1.以前公司有不一样.net项目组,有的项目是用SqlServer作数据库,有的项目是用Oracle,后面也有可能会用到Mysql等,并且要考虑后续扩展成主从库、多库的需求。(其实无论有没有这个需求,Dapper的封装应当像NetDh框架里封装的那样使用);git
2.涉及日志操做类的设计,须要记录用户操做日志、记录系统异常日志等;github
3.涉及缓存操做类的设计,这点不用需求都应该当考虑,不论是小项目的内存缓存仍是大项目中的Redis/Memcache等;sql
4.涉及二次开发模式简单的设计。由于多个客户须要同一个项目产品,可是客户之间对该产品的需求点又有些不同。数据库
本文先讲为了第1点需求而封装的数据库操做类,其它三点在接下来文章中也会介绍。json
Dapper是轻量级高效的框架,它高效缘由是利用Emit技术+各类解析缓存+DataReader。缓存
Dapper能够在全部Ado.net Providers下工做,包括SQL Server, Oracle, MySQL , SQLite, PostgreSQL, sqlce, firebird 等,这些数据库操做类都有实现IDbConnection接口。你看源码会发现,Dapper提供的public方法大都是对IDbConnection的扩展。 app
DapperExtensions是Dapper的第三方插件之一(NetDh框架是用Dapper+DapperExtensions的组合),Dapper经常使用的代码是 Query<T>(selectSql..)把数据库获取的记录转成实体类对象,而DapperExtensions是封装了Dapper,支持诸如 Get<T>(id)、Insert<T>、Update<T>等函数,可让你不写sql就能简单操做数据库数据。框架
如上图,NetDh框架是把Dapper(.net4.5.1)目前最新源码和DapperExtensions源码合并在同一个程序集,而后添加到解决方案。有源码就能够随便调试,深刻了解Dapper和学习Dapper。ide
在NetDh.DbUtility程序集中,DbHandleBase是个抽象基类,它封装了“数据库经常使用的操做函数”,以下图:
DbHandleBase基类封装了Dapper+DapperExtensions,若是要实现SqlServerHandle操做类,那么只要继承DbHandleBase,而后重写基类的CreateConnection抽象函数(为了获取链接对象),便可拥有这些“数据库经常使用的操做函数”。其它数据库类型也同样,很简单吧。其中,Oracle操做类不使用微软早期的OracleClient(微软已经不维护),而是使用Oracle官方ODP.NET(nuget下载 Oracle.ManagedDataAccess.dll),不用再安装OraInsClient和配置tnsnames.ora。
用了Dapper通常就是不用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(); } }
以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 } }
好比你的实体类名是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 });
国外有github,国内有码云,在国内使用码云速度很是快。NetDh框架源码放在码云上: