上一篇文章收获了 140 多条评论,这是咱们始料未及的。html
向来有争议的话题都是公说公的理,婆说婆的理,Entity Framework的爱好者对此能够说是嗤之以鼻,不屑一顾,而Dapper爱好者则是举双手同意,阅之大快人心。sql
每一个人不一样的阅历,社会经验,甚至对简繁的偏见都会影响对此事的见解,凡事都有优劣,取其精华而弃之糟泊,方为上策。数据库
这篇文章则将目光聚焦到Dapper。数组
Dapper是如此的简单,她只提供了 3 个帮助函数:浏览器
而在实际的项目中,咱们可能只会用到强类型列表,因此上面列出的 3 个帮助函数只会用到 2 个。app
有人说了,简单其实就意味着复杂,的确如此。ide
过少的封装意味着每次可能要书写过多的重复代码,所以每一个Dapper开发者可能都会自行扩展一些用着顺手的方法,也就不足为奇了,俗话说一千我的眼里有一千个哈姆雷特。函数
下面我会分享在将 AppBoxPro 从 EntityFramework 迁移到 Dapper 中遇到的问题,以及解决方法,其中也包含个人小小封装,但愿你能喜欢。ui
下面是 AppBoxPro.Dapper 的项目开发截图:spa
咱们对模型有两个约定:
1. IKeyID接口
2. NotMapped特性
来看一下 User 模型的声明:
public class User : IKeyID { [Key] public int ID { get; set; } [Required, StringLength(50)] public string Name { get; set; } [Required, StringLength(100)] public string Email { get; set; } public int? DeptID { get; set; } [NotMapped] public string UserDeptName { get; set; } }
其中 IKeyID 是一个接口,定义了模型类必须包含名为 ID 的属性,这个接口是为了计算 FineUIPro 控件中模拟树的下拉列表和表格的数据源。
NotMapped特性代表这个属性没有数据库映射,仅仅做为一个内存中使用的属性,通常有两个用途:
1. 表关联属性,好比 User 模型中的 UserDeptName 属性,在数据库检索时能够经过 inner join 将 Dept 表的 Name 属性映射于此。
2. 内存中计算的值,好比在 Dept 模型中的 TreeLevel, Enabled, IsTreeLeaf,用于在模拟树的表格中肯定节点的层次结构和节点属性。
若是你查阅 Dapper 的文档,你会发现一个常见的操做代码段:
using (var conn = new MySqlConnection(connectionString)) { connection.Open(); var users = conn.Query<User>("select * from users"); // ... }
虽然看起来简单,可是若是每个地方都有加个 using 代码段,势必也会影响观感和书写体验。
另外一方面,一个缩进的代码段会建立一个变量做用域,有时咱们可能会但愿在 using 外部获取某个变量,这就变成了:
IEnumerable<User> users; using (var conn = new MySqlConnection(connectionString)) { connection.Open(); users = conn.Query<User>("select * from users"); // ... }
这样写起来就会感受磕磕绊绊,一点都不美好了。
为了简化代码,咱们遵循以前的逻辑,一个请求一个数据库链接,将 IDbConnection 保存到 HttpContext 上下文中:
public static IDbConnection DB { get { if (!HttpContext.Current.Items.Contains("__AppBoxProContext")) { HttpContext.Current.Items["__AppBoxProContext"] = GetDbConnection(); } return HttpContext.Current.Items["__AppBoxProContext"] as IDbConnection; } }
public static IDbConnection GetDbConnection() { IDbConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["MySQL"].ToString()); connection.Open(); return connection; }
而后在请求结束时销毁这个链接,在 Global.asax 中:
protected virtual void Application_EndRequest() { var context = HttpContext.Current.Items["__AppBoxProContext"] as IDbConnection; if (context != null) { context.Dispose(); } }
通过这个简单的封装,上面的获取用户列表的代码能够直接写了:
var users = conn.Query<User>("select * from users");
在项目中,咱们可能常常须要经过 ID 来检索对象,在 Dapper 中实现很简单:
User current = DB.QuerySingleOrDefault<User>("select * from users where ID = @UserID", new { UserID = id });
可是因为这个操做常常用到,咱们可能须要屡次的拷贝粘贴,而仅仅修改其中的几个字符串。
当事情变得再也不美好时,咱们就要重构了,此次的提取公共方法没有任何难度:
protected T FindByID<T>(int paramID) { return FindByID<T>(DB, paramID); } protected T FindByID<T>(IDbConnection conn, int paramID) { // 约定:类型 User 对应的数据库表名 users var tableName = typeof(T).Name.ToLower() + "s"; return conn.QuerySingleOrDefault<T>("select * from "+ tableName +" where ID = @ParamID", new { ParamID = paramID }); }
能够看到其中的注释,一个模型类到数据库表的约定:User 模型对应于数据库表名 users,这个约定有助于咱们使用泛型,将参数强类型化(User)而无需传递字符串(users)。
通过此次的改造,经过ID检索对象就简单多了:
User current = FindByID<User>(id);
相关页面展现(用户编辑):
插入和更新是常见的数据库操做,好比对菜单项的操做涉及对 menus 表的插入和更新:
Menu item = new Menu(); item.Name = tbxName.Text.Trim(); item.NavigateUrl = tbxUrl.Text.Trim(); item.SortIndex = Convert.ToInt32(tbxSortIndex.Text.Trim()); item.Remark = tbxRemark.Text.Trim(); DB.Execute("insert menus(Name, NavigateUrl, SortIndex, ImageUrl, Remark, ParentID, ViewPowerID) values (@Name, @NavigateUrl, @SortIndex, @ImageUrl, @Remark, @ParentID, @ViewPowerID);", item);
首先初始化一个 Menu 模型对象,而后从页面上获取属性值并赋值到模型对象,最后经过 Dapper 提供的 Execute 方法执行插入操做。
相应的,更新操做须要首先经过菜单ID获取菜单模型对象,而后更新数据库:
Menu item = FindByID<Menu>(menuID); item.Name = tbxName.Text.Trim(); item.NavigateUrl = tbxUrl.Text.Trim(); item.SortIndex = Convert.ToInt32(tbxSortIndex.Text.Trim()); item.ImageUrl = tbxIcon.Text; item.Remark = tbxRemark.Text.Trim(); DB.Execute("update menus set Name = @Name, NavigateUrl = @NavigateUrl, SortIndex = @SortIndex, ImageUrl = @ImageUrl, Remark = @Remark, ParentID = @ParentID, ViewPowerID = @ViewPowerID where ID = @ID;", item);
上面的插入和更新操做存在两个不方便的地方:
1. SQL语句中要包含多个要更新的属性,容易遗漏和写错
2. 插入和更新的属性列表相同时,写法却彻底不一样,不方便拷贝粘贴
为了克服上面两个弱点,咱们对插入更新进行了简单的封装,为了避免手工填写属性列表,咱们须要一个从模型类读取属性列表的方法:
private string[] GetReflectionProperties(object instance) { var result = new List<string>(); foreach (PropertyInfo property in instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) { var propertyName = property.Name; // NotMapped特性 var notMappedAttr = property.GetCustomAttribute<NotMappedAttribute>(false); if (notMappedAttr == null && propertyName != "ID") { result.Add(propertyName); } } return result.ToArray(); }
上面函数经过反射获取实例对应模型类(instance.GetType())的属性列表(GetProperties()),而后过滤掉 ID 属性和拥有 NotMapped 标注的属性,最后返回属性数组。
对插入操做的封装:
protected void ExecuteInsert<T>(object instance, params string[] fields) { return ExecuteInsert<T>(DB, instance, fields); } protected void ExecuteInsert<T>(IDbConnection conn, object instance, params string[] fields) { // 约定:类型 User 对应的数据库表名 users string tableName = typeof(T).Name.ToLower() + "s"; if (fields.Length == 0) { fields = GetReflectionProperties(instance); } var fieldsSql1 = String.Join(",", fields); var fieldsSql2 = String.Join(",", fields.Select(field => "@" + field)); var sql = String.Format("insert {0} ({1}) values ({2});", tableName, fieldsSql1, fieldsSql2); return conn.Execute(sql, instance); }
ExecuteInsert 方法接受以下参数:
1. 类型 T:经过模型类名称获取数据库表名,这是一个命名约定
2. instance:模型实例,须要插入到数据对应表中
3. fields:这是一个可变参数,若是未传入 fields 参数,则经过前面定义的 GetReflectionProperties 函数获取模型类的所有属性列表
最后,通过简单的字符串拼接,就能方便的生成须要的SQL语句,并执行 Dapper 的 Execute 来插入数据了。
使用 ExecuteInsert 方法,咱们能够将上面的插入操做简化为:
ExecuteInsert<Menu>(item, "Name", "NavigateUrl", "SortIndex", "ImageUrl", "Remark", "ParentID", "ViewPowerID");
或者,直接这样写:
ExecuteInsert<Menu>(item);
是否是很方便。
一样,对更新的操做也是相似的,只不过在封装时拼接SQL字符串的逻辑稍微不一样:
protected void ExecuteUpdate<T>(object instance, params string[] fields) { return ExecuteUpdate<T>(DB, instance, fields); } protected void ExecuteUpdate<T>(IDbConnection conn, object instance, params string[] fields) { // 约定:类型 User 对应的数据库表名 users string tableName = typeof(T).Name.ToLower() + "s"; if (fields.Length == 0) { fields = GetReflectionProperties(instance); } var fieldsSql = String.Join(",", fields.Select(field => field + " = @" + field)); var sql = String.Format("update {0} set {1} where ID = @ID", tableName, fieldsSql); return conn.Execute(sql, instance); }
使用封装后的 ExecuteUpdate 方法,上面的更新操做能够简化为:
ExecuteUpdate<Menu>(item);
相关页面展现(用户角色页面):
有时,插入新的数据以后,咱们须要当即获取新插入数据的ID属性,方便后续的数据库操做,这就要对上面的 ExecuteInsert 进行改造,在 insert 语句以后加上以下SQL语句:
select last_insert_id();
上面的SQL语句仅适用于 MySQL 数据库,固然对于其余数据库也不难支持,后面会讲解。更新后的 ExecuteInsert 方法以下:
protected int ExecuteInsert<T>(object instance, params string[] fields) { return ExecuteInsert<T>(DB, instance, fields); } protected int ExecuteInsert<T>(IDbConnection conn, object instance, params string[] fields) { // 约定:类型 User 对应的数据库表名 users string tableName = typeof(T).Name.ToLower() + "s"; if (fields.Length == 0) { fields = GetReflectionProperties(instance); } var fieldsSql1 = String.Join(",", fields); var fieldsSql2 = String.Join(",", fields.Select(field => "@" + field)); var sql = String.Format("insert {0} ({1}) values ({2});", tableName, fieldsSql1, fieldsSql2); sql += "select last_insert_id();"; return conn.QuerySingle<int>(sql, instance); }
调用时,能够直接拿到新增行的ID,而后执行其余数据库操做:
// 插入用户 var userID = ExecuteInsert<User>(item); // 更新用户所属角色 DB.Execute("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", new { UserID = userID, RoleID = 101 });
分页和排序是使用 Dapper 的一个拦路虎,由于不少初学者一看到 Dapper 竟然没有内置的分页功能就放弃了,至少对于 5 年前的我也遭遇了一样的经历。
这是彻底没有必要的!
由于分页和排序彻底是标准的SQL语句是事情,Dapper没有义务为此负责。
咱们能够经过简单的封装化腐朽为神奇,来看看过滤,分页和排序也能如此简单和优雅,这个过程通常能够分解为 3 个步骤:
1. 添加过滤条件(好比匹配名称中的关键词,只列出启用的行....)
2. 获取总记录行数(数据库分页须要在页面显示总记录数,已经当前页的记录其实序号)
3. 获取当前分页的数据
下面是 AppBoxPro 中角色列表页面的过滤,分页和排序代码,咱们能够一目了然:
// 查询条件 var builder = new WhereBuilder(); string searchText = ttbSearchMessage.Text.Trim(); if (!String.IsNullOrEmpty(searchText)) { builder.AddWhere("Name like @SearchText"); builder.AddParameter("SearchText", "%" + searchText + "%"); } // 获取总记录数(在添加条件以后,排序和分页以前) Grid1.RecordCount = Count<Role>(builder); // 排列和数据库分页 Grid1.DataSource = SortAndPage<Role>(builder, Grid1); Grid1.DataBind();
上面的涉及三个重要的自定义类和函数:
1. WhereBuilder:咱们封装的一个简单的类,主要目的是将查询条件,条件参数以及SQL语句 3 则封装在一块儿。
2. Count:用来返回总记录数。
3. SortAndPage:用来执行分页和排序。
首先来看下WhereBuilder:
public class WhereBuilder { private DynamicParameters _parameters = new DynamicParameters(); public DynamicParameters Parameters { get { return _parameters; } set { _parameters = value; } } private List<string> _wheres = new List<string>(); public List<string> Wheres { get { return _wheres; } set { _wheres = value; } } private string _fromSql = String.Empty; public string FromSql { get { return _fromSql; } set { _fromSql = value; } } public void AddWhere(string item) { _wheres.Add(item); } public void AddParameter(string name, object value) { _parameters.Add(name, value); } }
其中:
1. _wheres: 对应于SQL的 where 子语句。
2. _parameters: 对应于 where 子语句用到的实际参数。
3. _fromSql: 若是省略此属性,则从模型类名推导出须要操做的数据库表名,对于须要进行表关联的复杂查询,则须要设置此参数,后面会进行详细讲解。
Count 的函数定义:
protected int Count<T>(WhereBuilder builder) { return Count<T>(DB, builder); } protected int Count<T>(IDbConnection conn, WhereBuilder builder) { var sql = builder.FromSql; if (String.IsNullOrEmpty(sql)) { // 约定:类型 User 对应的数据库表名 users sql = typeof(T).Name.ToLower() + "s"; } sql = "select count(*) from " + sql; if (builder.Wheres.Count > 0) { sql += " where " + String.Join(" and ", builder.Wheres); } return conn.QuerySingleOrDefault<int>(sql, builder.Parameters); }
SortAndPage的函数定义:
protected IEnumerable<T> SortAndPage<T>(WhereBuilder builder, FineUIPro.Grid grid) { return SortAndPage<T>(DB, builder, grid); } protected IEnumerable<T> SortAndPage<T>(IDbConnection conn, WhereBuilder builder, FineUIPro.Grid grid) { // sql: users // sql: select * from users // sql: select onlines.*, users.Name UserName from onlines inner join users on users.ID = onlines.UserID var sql = builder.FromSql; if (String.IsNullOrEmpty(sql)) { // 约定:类型 User 对应的数据库表名 users sql = typeof(T).Name.ToLower() + "s"; } if (!sql.StartsWith("select")) { sql = "select * from " + sql; } if (builder.Wheres.Count > 0) { sql += " where " + String.Join(" and ", builder.Wheres); } sql += " order by " + grid.SortField + " " + grid.SortDirection; sql += " limit @PageStartIndex, @PageSize"; builder.Parameters.Add("PageSize", grid.PageSize); builder.Parameters.Add("PageStartIndex", grid.PageSize * grid.PageIndex); return conn.Query<T>(sql, builder.Parameters); }
上面的封装很简单,对分页的处理只有这三行代码:
sql += " limit @PageStartIndex, @PageSize"; builder.Parameters.Add("PageSize", grid.PageSize); builder.Parameters.Add("PageStartIndex", grid.PageSize * grid.PageIndex);
固然这里的 limit 子句只适用于 MySQL,其余数据库的用法后面会有介绍。
对于 builder.FromSql 属性,若是留空,则检索当前数据表的所有数据。而对于表关联查询,能够设置完整的 select 子句,下面会进行介绍。
在线用户列表页面,对于某个用户,咱们不只要列出用户的登陆时间,最后操做时间,IP地址,还要列出用户名和用户中文名称。
这里就须要用到表关联,由于 onlines 只记录用户ID,而用户名称须要从 users 表获取,下面就是此页面的过滤,分页和排序逻辑:
// 查询条件 var builder = new WhereBuilder(); string searchText = ttbSearchMessage.Text.Trim(); if (!String.IsNullOrEmpty(searchText)) { builder.AddWhere("users.Name like @SearchText"); builder.AddParameter("SearchText", "%" + searchText + "%"); } DateTime twoHoursBefore = DateTime.Now.AddHours(-2); builder.AddWhere("onlines.UpdateTime > @TwoHoursBefore"); builder.AddParameter("TwoHoursBefore", twoHoursBefore); // 获取总记录数(在添加条件以后,排序和分页以前) Grid1.RecordCount = Count<Online>(builder); // 排列和数据库分页 builder.FromSql = "select onlines.*, users.Name UserName, users.ChineseName UserChineseName from onlines inner join users on users.ID = onlines.UserID"; Grid1.DataSource = SortAndPage<Online>(builder, Grid1); Grid1.DataBind();
相关页面展现(用户列表):
Dapper对事务有两种支持,一种是直接在 Query 或者 Execute 中传递 transaction 对象,而另一种则更加简单。
在更新用户信息时,首先是更新 users 表,而后还要操做用户角色表和用户部门表,对于多个数据表的屡次操做,能够放到一个事务中:
using (var transactionScope = new TransactionScope()) { // 更新用户 ExecuteUpdate<User>(DB, item); // 更新用户所属角色 int[] roleIDs = StringUtil.GetIntArrayFromString(hfSelectedRole.Text); DB.Execute("delete from roleusers where UserID = @UserID", new { UserID = userID }); DB.Execute("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", roleIDs.Select(u => new { UserID = userID, RoleID = u }).ToList()); // 更新用户所属职务 int[] titleIDs = StringUtil.GetIntArrayFromString(hfSelectedTitle.Text); DB.Execute("delete from titleusers where UserID = @UserID", new { UserID = userID }); DB.Execute("insert titleusers (UserID, TitleID) values (@UserID, @TitleID)", titleIDs.Select(u => new { UserID = userID, TitleID = u }).ToList()); transactionScope.Complete(); }
相关页面展现(角色权限):
Dapper支持方便的传入匿名参数,前面已经屡次看到,好比下面这个更新用户角色的代码:
DB.Execute("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", new { UserID = userID, RoleID = 101 });
不只如此,Dapper还支持屡次执行一个命令,只须要传入一个匿名数组便可。
在 AppBoxPro 中,有多处应用场景,好比前面的更新用户角色的代码:
DB.Execute("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", roleIDs.Select(u => new { UserID = userID, RoleID = u }).ToList());
这里经过 Select 表达式获取一个动态对象数组。
在 ConfigHelper 中,咱们还有手工建立匿名数组的场景,用来更新 configs 表中的多个行数据:
DB.Execute("update configs set ConfigValue = @ConfigValue where ConfigKey = @ConfigKey", new[] { new { ConfigKey = "Title", ConfigValue = Title }, new { ConfigKey = "PageSize", ConfigValue = PageSize.ToString() }, new { ConfigKey = "Theme", ConfigValue = Theme }, new { ConfigKey = "HelpList", ConfigValue = HelpList }, new { ConfigKey = "MenuType", ConfigValue = MenuType } });
多数据库支持真的不难,在咱们支持的 MySQL 和 SQLServer 两个数据库中,只有少数几处须要特殊处理。
1. 数据库链接,咱们能够根据 ProviderName 来生成不一样的 IDbConnection 实例。
首先来看下 Web.config 中数据库相关的配置节:
<appSettings> <!-- 须要链接的数据库,对应于 connectionStrings 节的 name 属性 --> <add key="Database" value="MySQL" /> </appSettings> <connectionStrings> <clear /> <add name="SQLServer" connectionString="Password=pass;Persist Security Info=True;User ID=sa;Initial Catalog=appbox;Data Source=." providerName="System.Data.SqlClient" /> <add name="MySQL" connectionString="Server=localhost;Database=appbox;Uid=root;Pwd=pass;Charset=utf8" providerName="MySql.Data.MySqlClient" /> </connectionStrings>
而后是对 GetDbConnection 的扩展:
public static IDbConnection GetDbConnection()
{
var database = ConfigurationManager.AppSettings["Database"];
var connectionStringSection = ConfigurationManager.ConnectionStrings[database];
var connectionString = connectionStringSection.ToString();
IDbConnection connection;
if (connectionStringSection.ProviderName.StartsWith("MySql"))
{
connection = new MySqlConnection(connectionString);
}
else
{
connection = new SqlConnection(connectionString);
}
// 打开数据库链接
connection.Open();
return connection;
}
2. 插入后获取新增的行ID
protected int ExecuteInsert<T>(IDbConnection conn, object instance, params string[] fields) { // 约定:类型 User 对应的数据库表名 users string tableName = typeof(T).Name.ToLower() + "s"; if (fields.Length == 0) { fields = GetReflectionProperties(instance); } var fieldsSql1 = String.Join(",", fields); var fieldsSql2 = String.Join(",", fields.Select(field => "@" + field)); var sql = String.Format("insert {0} ({1}) values ({2});", tableName, fieldsSql1, fieldsSql2); if (conn is MySqlConnection) { sql += "select last_insert_id();"; } else { sql += "SELECT @@IDENTITY;"; } return conn.QuerySingle<int>(sql, instance); }
3. 数据库分页处理,更新后的 SortAndPage 函数:
protected IEnumerable<T> SortAndPage<T>(IDbConnection conn, WhereBuilder builder, FineUIPro.Grid grid) { var sql = builder.FromSql; if (String.IsNullOrEmpty(sql)) { // 约定:类型 User 对应的数据库表名 users sql = typeof(T).Name.ToLower() + "s"; } if (!sql.StartsWith("select")) { sql = "select * from " + sql; } if (builder.Wheres.Count > 0) { sql += " where " + String.Join(" and ", builder.Wheres); } sql += " order by " + grid.SortField + " " + grid.SortDirection; // 分页 if (conn is MySqlConnection) { sql += " limit @PageStartIndex, @PageSize"; } else { sql += " OFFSET @PageStartIndex ROWS FETCH NEXT @PageSize ROWS ONLY"; } builder.Parameters.Add("PageSize", grid.PageSize); builder.Parameters.Add("PageStartIndex", grid.PageSize * grid.PageIndex); return conn.Query<T>(sql, builder.Parameters); }
好了,上面就是所有的多数据库处理代码了。相比 jQuery 对不一样浏览器的封装,这里的多数据库支持真是的小巫见大巫了。
这篇文章主要描述了从 Entity Framework 迁移到 Dapper 时遇到的问题,以及咱们给出的简单封装,但愿你能喜欢。
注:AppBox非免费软件,若是你但愿获取以下版本和后续更新,请加入【三石和他的朋友们】付费知识星球下载源代码:http://fineui.com/fans/