今天成功把.Net Framework下使用Dapper進行封裝的ORM成功遷移到.Net Core 2.0上,在遷移的過程中也遇到一些很有意思的問題,值得和大家分享一下。下面我會還原遷移的每一個過程,以及在此過程中遇到的問題和處理這些問題的方法。
一、遷移前的准備
之前對Dapper的封裝使用的是.Net Framework下的ORM 框架Dapper,開發工具VS2013,現在既然想在.Net Core2.0上使用Dapper,我要先到NuGet看看有沒有支持 .Net Core的,在Nuget找到如下:
果然有!!!因為項目中使用的是MySQL,所以還要看看有沒有MySQL的.Net驅動,發現也有,但是是預發行版本,算了等不及正式版了,先用(生產環境中我暫時沒使用)它來測試,等正式版出來就正式遷移了(* ̄︶ ̄)
好了,該准備的已經准備好了,下面就是使用VS2017新建一個項目,用來測試,項目的整體結構如下:
二、正式遷移
.Net Framework下對Dapper進行的二次封裝,代碼部分如下,后面會介紹我為什么要這樣封裝:
namespace ZSZ.Core.Respository { public interface IDataAdapter { string BindVariablePrefix { get; } void AppendColumnName(StringBuilder sb, string columnName); void AppendColumnNameEqualsValue(StringBuilder sb, string columnName); void AppendUpdateColumnName(StringBuilder sb, string columnName); } public class OracleDataAdapter : IDataAdapter { public string BindVariablePrefix { get { return ":"; } } public void AppendColumnName(StringBuilder sb, string columnName) { sb.AppendFormat("{0}, ", columnName.ToUpper()); } public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) { sb.AppendFormat("{0}{1}, ", BindVariablePrefix, columnName.ToUpper()); } public void AppendUpdateColumnName(StringBuilder sb, string columnName) { sb.AppendFormat("{0}={1}{0}, ", columnName.ToUpper(), BindVariablePrefix); } } public class SqlServerDataAdapter : IDataAdapter { public string BindVariablePrefix { get { return "@"; } } public void AppendColumnName(StringBuilder sb, string columnName) { sb.AppendFormat("{0}, ", columnName.ToUpper()); } public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) { sb.AppendFormat("{0}{1}, ", BindVariablePrefix, columnName.ToUpper()); } public void AppendUpdateColumnName(StringBuilder sb, string columnName) { sb.AppendFormat("{0}={1}{0}, ", columnName.ToUpper(), BindVariablePrefix); } } public class MySqlDataAdapter : IDataAdapter { public string BindVariablePrefix { get { return "@"; } } public void AppendColumnName(StringBuilder sb, string columnName) { sb.AppendFormat("{0}, ", columnName.ToUpper()); } public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) { sb.AppendFormat("{0}{1}, ", BindVariablePrefix, columnName.ToUpper()); } public void AppendUpdateColumnName(StringBuilder sb, string columnName) { sb.AppendFormat("{0}={1}{0}, ", columnName.ToUpper(), BindVariablePrefix); } } public static class DataBase { internal class TypeInsertPair { public string Columns { get; set; } public string Values { get; set; } } /* * * * 線程安全:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。 * http://www.cnblogs.com/CreateMyself/p/6086752.html * http://www.cnblogs.com/PurpleTide/archive/2011/11/21/2256577.html * http://www.cnblogs.com/lori/p/4344026.html */ private static readonly ConcurrentDictionary<RuntimeTypeHandle, TypeInsertPair> TypeInsertPairDictionary = new ConcurrentDictionary<RuntimeTypeHandle, TypeInsertPair>(); private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> TypeUpdateDictionary = new ConcurrentDictionary<RuntimeTypeHandle, string>(); private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> TypeColumnsDictionary = new ConcurrentDictionary<RuntimeTypeHandle, string>(); private static IDataAdapter defaultDataAdapter; private static IDataAdapter DefaultDataAdapter { get { if (defaultDataAdapter == null) { defaultDataAdapter = GetDataAdapter(DefaultConnectionStringSettings); } return defaultDataAdapter; } set { defaultDataAdapter = value; } } public static IDataAdapter GetDataAdapter(this ConnectionStringSettings connectionStringSettings) { if (connectionStringSettings == null) { return defaultDataAdapter; } if (string.IsNullOrEmpty(connectionStringSettings.ProviderName)) { throw new Exception("數據庫連接串的配置不正確!"); } if (connectionStringSettings.ProviderName.ToLower().Contains("oracle")) { return new OracleDataAdapter(); } else if (connectionStringSettings.ProviderName.ToLower().Contains("mysql")) { return new MySqlDataAdapter(); } else if (connectionStringSettings.ProviderName.ToLower().Contains("sql")) { return new SqlServerDataAdapter(); } throw new Exception("暫不支持您使用的數據庫類型!"); } private static ConnectionStringSettings defaultConnectionStringSettings; public static ConnectionStringSettings DefaultConnectionStringSettings { get { if (defaultConnectionStringSettings == null) { defaultConnectionStringSettings = ConfigurationManager.ConnectionStrings["db"]; } return defaultConnectionStringSettings; } set { if (value == null) throw new Exception("默認的數據庫連接配置信息不能為空!"); defaultConnectionStringSettings = value; DefaultDataAdapter = GetDataAdapter(value); } } private static IDbConnection GetDbConnection(this ConnectionStringSettings connectionStringSettings) { if (connectionStringSettings != null && (string.IsNullOrEmpty(connectionStringSettings.ConnectionString) || string.IsNullOrEmpty(connectionStringSettings.ProviderName))) throw new Exception("數據庫鏈接字符串配置不正確!"); var settings = connectionStringSettings == null ? DefaultConnectionStringSettings : connectionStringSettings; var factory = System.Data.Common.DbProviderFactories.GetFactory(settings.ProviderName); var connection = factory.CreateConnection(); connection.ConnectionString = settings.ConnectionString; return connection; } private static TypeInsertPair GetTypeInsertPair(this Type type, IDataAdapter adapter) { if (TypeInsertPairDictionary.ContainsKey(type.TypeHandle)) return TypeInsertPairDictionary[type.TypeHandle]; var columns = new StringBuilder(); var values = new StringBuilder(); foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (property.IsIgnore() && !"id".Equals(property.Name, StringComparison.OrdinalIgnoreCase)) continue; adapter.AppendColumnName(columns, property.Name); adapter.AppendColumnNameEqualsValue(values, property.Name); } var pair = new TypeInsertPair() { Columns = columns.ToString().Substring(0, columns.Length - 2), Values = values.ToString().Substring(0, values.Length - 2) }; TypeInsertPairDictionary[type.TypeHandle] = pair; return pair; } private static string GetTypeColumns(this Type type, ConnectionStringSettings connectionStringSettings) { if (TypeColumnsDictionary.ContainsKey(type.TypeHandle)) return TypeColumnsDictionary[type.TypeHandle]; var sb = new StringBuilder(); var adapter = connectionStringSettings.GetDataAdapter(); foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { //查詢時的字段 adapter.AppendColumnName(sb, property.Name); } var columns = sb.ToString().Substring(0, sb.Length - 2); TypeColumnsDictionary[type.TypeHandle] = columns; return columns; } private static string GetTypeUpdateSetString(this Type type, ConnectionStringSettings connectionStringSettings) { if (TypeUpdateDictionary.ContainsKey(type.TypeHandle)) return TypeUpdateDictionary[type.TypeHandle]; var sb = new StringBuilder(); var adapter = connectionStringSettings.GetDataAdapter(); foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { //更新時如果傳入實體對象的話,會有ID在里面,所以在這里要把ID(主鍵)去掉 if (property.IsIgnore() || "id".Equals(property.Name, StringComparison.OrdinalIgnoreCase)) continue; adapter.AppendUpdateColumnName(sb, property.Name); } var update = sb.ToString().Substring(0, sb.Length - 2); TypeUpdateDictionary[type.TypeHandle] = update; return update; } //如果對應的字段上有這樣的特性就不參與對應的數據庫操作 private static bool IsIgnore(this PropertyInfo property) { var attribute = property.GetCustomAttributes(typeof(IgnoreAttribute), true).FirstOrDefault() as IgnoreAttribute; return attribute != null && attribute.Ignore; } #region 查詢 //根據實體生成sql,映射返回實體集合 //使用:傳過來condition、param參數即可 public static IEnumerable<T> Get<T>(string condition = null, object param = null, string tableName = null, ConnectionStringSettings connectionStringSettings = null, IDbTransaction transaction = null) where T : class { if (string.IsNullOrEmpty(tableName) && !(typeof(T).IsSubclassOf(typeof(BaseEntity<T>)))) throw new Exception("沒有輸入表名時只支持數據庫實體查詢!"); var name = string.IsNullOrEmpty(tableName) ? BaseEntity<T>.TableName : tableName; var columns = string.IsNullOrEmpty(tableName) ? "*" : typeof(T).GetTypeColumns(connectionStringSettings); var sql = string.IsNullOrEmpty(condition) ? string.Format("select {0} from {1}", columns, name) : string.Format("select {0} from {1} where {2}", columns, name, condition); var conn = connectionStringSettings.GetDbConnection(); return conn.Query<T>(sql, param, transaction); } //根據SQL映射實體或ViewModel //使用:傳過來SQL,讓Dapper進行映射 public static IEnumerable<T> GetBySql<T>(string sql, object param = null, ConnectionStringSettings connectionStringSettings = null, IDbTransaction transaction = null) where T : class { var conn = connectionStringSettings.GetDbConnection(); return conn.Query<T>(sql, param, transaction); } //根據ID獲取單個實體對象 //使用:傳過來ID public static T GetById<T>(string id, string tableName = null, ConnectionStringSettings connectionStringSettings = null, IDbTransaction transaction = null) where T : class { var adapter = connectionStringSettings.GetDataAdapter(); return Get<T>(connectionStringSettings: connectionStringSettings, tableName: tableName, condition: string.Format("id ={0}id", adapter.BindVariablePrefix), param: new { id = id }, transaction: transaction).FirstOrDefault(); } }
在把.Net Framework下對Dapper進行二次封裝的代碼放到.Net Core 2.0之前,我們需要在上面新建的項目中引用,如下Nuget包:
(1)
在圖中標識的項目中需要引用Dapper和MySQL
1)Install-Package MySql.Data -Version 8.0.8-dmr
2)Install-Package Dapper -Version 1.50.2
(2)遷移到.Net Core2.0上之后報錯截圖,前提是有些可以手動引用,下面列出來的是ctrl+.還解決不了的。
1)在.Net Framework下 System.Configuration下有ConnectionStringSettings類,但是在.Net Core中是不是還在同樣的命名空間下?於是查看接口文檔https://docs.microsoft.com/zh-cn/dotnet/api/system.configuration.connectionstringsettings?view=netcore-2.0,發現,真有!!!於是就using System.Configuration;同時還需要安裝Nuget包: Install-Package System.Configuration.ConfigurationManager,這樣就可以把ConnectionStringSettings類的錯誤解決掉。
2)ConfigurationManager類的錯誤,關於讀取配置文件中的信息,可以添加 Microsoft.Extensions.Configuration和Microsoft.Extensions.Configuration.Json來解決配置文件的讀取問題,還可以使用原始的方式來讀取,但需要添加 System.Configuration.ConfigurationManager,關於怎么讀取配置文件中的信息,很簡單,在這里就不介紹了。下面給一篇關於如何讀取配置文件信息的文章 :http://www.cnblogs.com/mantgh/p/7425113.html
3)在.Net Framework下, system.Data.Common下有DbProviderFactories類,如下圖:
但是在.Net Core 2.0的接口文檔中沒有找到該類,那該怎么辦?首先在這里講點ADO.Net的相關知識,我為什么要使用該類?因為使用該類中的 public static DbProviderFactory GetFactory(string providerInvariantName);可以通過providerInvariantName來創建對應的 ClientFactory,因為該方法返回DbProviderFactory ,同時SqlClientFactory、MySqlClientFactory等都繼承DbProviderFactory,如下圖所示:
我們通過該方法拿到對應的DbProviderFactory工廠了 ,也就意味着可以通過providerInvariantName拿到對應數據庫的ClientFactory,然后調用里面的CreateConnection()方法就可以得到一個DbConnection,再調用里面的ConnectionString屬性,把鏈接字符串賦值給該屬性即可。該部分的代碼在上面測試項目System.Data.CommonExts中,代碼如下:
1 using MySql.Data.MySqlClient; 2 using System.Data.Common; 3 using System.Data.SqlClient; 4 5 namespace System.Data.CommonExts 6 { 7 public static class DbProviderFactories 8 { 9 /// <summary> 10 /// 通過在appsettings.json文件中配置 "providerName",來創建對應的數據庫鏈接 11 /// </summary> 12 /// <param name="providerInvariantName">例如:MySql.Data.MySqlClient</param> 13 /// <returns>DbProviderFactory</returns> 14 public static DbProviderFactory GetFactory(string providerInvariantName) 15 { 16 if (string.IsNullOrEmpty(providerInvariantName)) throw new Exception("數據庫鏈接字符串配置不正確!"); 17 18 if(providerInvariantName.ToLower().Contains("mysql")) 19 { 20 return new MySqlClientFactory(); 21 } 22 else if(providerInvariantName.ToLower().Contains("sql")) 23 { 24 return SqlClientFactory.Instance; 25 } 26 27 throw new Exception("暫不支持您使用的數據庫類型!"); 28 29 } 30 } 31 }
注意,這里需要安裝的包,如下:
4)配置文件配置如下:
{ "db": { "mysql": { "conStr": "server=.;charset=gb2312;user id=root;password=123456;persist security info=True;database=dappertest;charset=utf8;", "providerName": "MySql.Data.MySqlClient" } } }
我知道你會想,為什么要這樣做???神經病吧!!!,如果你有好的辦法也可以分享出來,后面會介紹為什么我要這樣封裝。
好了,完成上面的三步即可完成遷移,下面是在測試時遇到的問題,在講測試時遇到的問題前,需要給大家介紹一下,為什么我要這樣封裝Dapper以及正式項目中為什么要這樣搭建。
三、對Dapper進行封裝的原因以及正式項目中搭建這樣的框架的背景
(1)如果不對Dapper進行二次封裝,我們是這樣使用的
using(MySqlConnection con = new MySqlConnection("server=127.0.0.1;database=test;uid=root;pwd=;charset='gbk'")) { var list=con.Query<User>("select * from user"); ...... ...... }
每次對數據庫操作,我都需要先new一個MySqlConnection,太煩。於是就有了,下面的代碼:
圖中的ConnectionStringSettings是沒有找到對應程序集時,自己定義的。現在在.Net Core2.0中可以找到了,上面已經介紹了。
(2)我現在使用的是MySQL數據庫,如果要切換數據庫,比如使用SqlServer、oracle等其他數據庫,我還需要修改connection,太麻煩,於是就有了下面的代碼:
通過DBProviderFactories,動態創建數據庫鏈接。
(3)在沒有對Dapper進行二次封裝,如果我們切換數據庫,由於不同數據庫的語法不一樣,修改的工作量不能忽視,如何屏蔽不同數據庫之間語法的不同呢,於是就有了下面的代碼:
1 public interface IDataAdapter 2 { 3 string BindVariablePrefix { get; } 4 void AppendColumnName(StringBuilder sb, string columnName); 5 void AppendColumnNameEqualsValue(StringBuilder sb, string columnName); 6 void AppendUpdateColumnName(StringBuilder sb, string columnName); 7 } 8 9 public class MySqlDataAdapter : IDataAdapter 10 { 11 public string BindVariablePrefix 12 { 13 get { return "@"; } 14 } 15 public void AppendColumnName(StringBuilder sb, string columnName) 16 { 17 sb.AppendFormat("{0}, ", columnName.ToUpper()); 18 } 19 20 public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 21 { 22 sb.AppendFormat("{0}{1}, ", BindVariablePrefix, columnName.ToUpper()); 23 } 24 25 public void AppendUpdateColumnName(StringBuilder sb, string columnName) 26 { 27 sb.AppendFormat("{0}={1}{0}, ", columnName.ToUpper(), BindVariablePrefix); 28 } 29 }
下面是真實項目的整體框架,如下圖:
不是說這樣的搭建是好的,可以適合任何的項目,只能說,它適合我,適合現在的需求。現在的web已經是一個泛化的web,網站不是web的全部分,只是web的一小部分。現在的產品是一個web產品矩陣,不僅包括網站而且還包括iOS、Android、微信、微信小程序等。所以把接口單獨分離出來,到時候可以單獨部署在一台服務器上,作為公共服務,不僅我們的網站可以使用,而且我們的小程序也可以使用。好了,有點扯了,說的不對的還請各位指出來。
四、測試時遇到的問題
(1)鏈接字符串server=.需要修改為:server=127.0.0.1; 否則會報鏈接不上數據庫的錯誤,這里就不截圖了。
(2)鏈接字符串需要加上SslMode=None
最后完整的配置文件如下:
1 { 2 "db": { 3 "mysql": { 4 "conStr": "server=127.0.0.1;charset=gb2312;user id=root;password=123456;persist security info=True;database=dappertest;charset=utf8;SslMode=None", 5 "providerName": "MySql.Data.MySqlClient" 6 } 7 } 8 }
講到這里基本上就講完了,大家如果遇到問題了,可以留言,我看到后會及時回復大家。
五、總結
通過上面的講解,我們不要為了使用ORM而使用ORM,而忘記了他們底層使用的是ADO.Net,把他們搞明白,比任何ORM都重要!!!,謝謝大家,希望對你有幫助!
六、補充
這里要指出來一點,使用System.Configuration.ConfigurationManager會導致無法跨平台,之前園子里有人介紹過如何使用ConfigurationManager,但是為了跨平台,不建議使用它。那如果不使用System.Configuration.ConfigurationManager,那ConnectionStringSettings就沒法使用了,所以需要自定義一個這樣的類,代碼如下:
using System; using System.Collections.Generic; using System.Text; namespace DapperMigrationServices { public class ConnectionStringSettings { public string ProviderName { get; set; } public string ConnectionString { get; set; } } }
一位網友分享的Dapper封裝,思想比我的好,大家可以借鑒一下,感謝能分享出來,多看看可以開拓視野:
https://github.com/xakepbean/Dapper-Extensions
作者:郭崢
出處:http://www.cnblogs.com/runningsmallguo/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。