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 这个接口。因此,我们可以在后面创建数据库连接对象时,通过不同的枚举值,创建针对不同数据库操作的数据库连接对象。
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 系列可能会不定期更新,希望大家同样可以多多关注啊。最后,感谢之前赞赏的小伙伴。
上一篇: 由dubbo服务禁用system.gc而引起的思考
下一篇: 带老婆看电影,也没什么毛病啊