ORM概念解析
首先梳理一下ORM的概念,ORM的全拼是Object Relation Mapping (物件關係對映),其中Object就是面嚮物件語言中的物件,本文使用的是c#語言,所以就是.net物件;Relation Mapping則是關係對映,在資料庫的系統中指的就是.net物件與資料庫表之間的關係對映。
為什麼.net物件與資料庫表之間要進行關係對映呢?
答案當然是顯而易見的,因為資料庫表的結構與物件的結構非常的相似,如果將兩者之間建立起對映關係,我們就可以很容易地在面嚮物件語言中像操作普通物件一樣去進行資料庫表的操作,那對程式設計師來說簡直就是福音。那麼這兩者之間的關係由誰來建立呢,畢竟不能讓它倆憑空去產生關係,當中要有“媒人”撮合,這個“媒人”就叫做ORM框架,也可以叫做資料庫持久層框架。
在上一篇 “造輪運動”之 ORM框架系列(一)~談談我在實際業務中的增刪改查 中,談了一下我遇到的增刪改查,並分析了原生sql、Lambda to sql、儲存過程等方式的利弊,在這其中我也慢慢地形成了自己對ORM框架的認識,也逐漸繪製出了一幅我心目中完美的ORM框架藍圖。
描繪藍圖之前先給我的ORM框架取個名字,叫做CoffeeSQL,這個名字的由來是因為我希望自己用了這個框架以後可以給我節省出工作中能享受一杯Coffee的時間~
1、實體對映
c#的物件想要與資料庫表建立對映關係,那就要記錄對映關係的配置資訊,我更喜歡採用一種一目瞭然的方式來進行配置:直接在類的欄位上標識Attribute。
基本使用方式如下程式碼所示:
1 /// <summary> 2 /// 實體類 3 /// </summary> 4 [Table("T_Students")] 5 public class Student : EntityBase 6 { 7 [PrimaryKey] 8 [Column] 9 public string Id { get; set; } 10 11 [Column("Name")] 12 public string StudentName { get; set; } 13 14 [Column] 15 public int Age { get; set; } 16 }
我們可以看到在這個實體類中我們使用了三個Attribute,分別為 TableAttribute(標識對映表)、 PrimaryKeyAttribute(標識主鍵)、CloumnAttribute(標識資料列),相信這些都很好理解。
2、lambda操作,增刪改查,強型別
在一些最基礎的資訊增刪改查功能中,對於單表的sql操作是非常頻繁的,我建議使用lambda的形式操作,第一是因為方便,第二是因為單表操作Lambda TO SQL轉化後的sql是最簡便的,所以不用擔心它的效能。
具體的程式碼操作如下:
1)增
1 //Add 2 dbContext.Add<Student>(new Student { Id = newId, StudentName = "王二", Age = 20 });
2)刪
1 //delete 2 dbContext.Delete<Student>(s => s.Id.Equals(newId));
3)改
1 //update 2 dbContext.Update<Student>(s => new { s.StudentName }, new Student { StudentName = name }) //更新欄位 3 .Where(s => s.Id.Equals(newId)) //where條件 4 .Done();
4)查
1 //select 2 dbContext.Queryable<Student>() 3 .Select(s => new { s.StudentName, s.Id }) //欄位查詢 4 .Where(s => s.Age > 10 && s.StudentName.Equals(name)) //where條件 5 .Paging(1, 10) //分頁查詢 6 .ToList();
3、原生sql操作,弱型別,非實體類欄位的儲存的方式 => 索引器
上面介紹的lambda表示式的操作方式只適用於單表的查詢,如果需要進行聯多表查詢或者需要進行更復雜的查詢,那麼我更傾向於進行原生sql的查詢。當然,原生sql的查詢也提供結果對映到物件的功能,類似於Dapper的用法。
具體操作如下:
1 //原生sql查詢用法 2 string complexSQL = "select xxx from t_xxx A,t_yyy B where xx = {0} and yy = {1}"; 3 object[] sqlParams = new Object[2] { DateTime.Now, 2 }; 4 var resList = dbContext.Queryable(complexSQL, sqlParams).ToList<Student>();
對上面的程式碼稍微進行一下解釋,complexSQL 中的 {0} 和 {1} 是引數化查詢引數的佔位符,在原生sql的查詢用法中你可以使用任意c#的基本變數去填充佔位符,最終獲得sql引數化查詢的效果,用法非常方便。
當然你可能會困惑:假如我查詢的結果中包含了不是Student物件中欄位的值怎麼辦?
答案是,你可以用索引的方式將非Student物件中欄位的值取出,下面做一個示範,假如你希望查出 xxx 欄位,你可以這樣做:
1 Student resList1 = resList[0]; 2 var segmentValue = (string)resList1["xxx"]; //查詢結果中的任何欄位值都可以以索引的方式查出,記得轉換為欄位的實際型別
是不是So Easy!
4、更復雜的sql邏輯,那得用儲存過程
製造生產類的企業的業務邏輯離不開流程加表單,表單的查詢與資料寫入就是屬於比較複雜的sql了。
但是這其實還不算什麼,更復雜的是,由於現在流行的大資料概念,車間的主管們往往也想沾沾邊,所以會在車間的大屏上展現各種資料統計的看板,這其中就涉及到巨多表的查詢邏輯。曾經開發過一個看板功能,特地數了一下,單單一條sql就接近200行的程式碼量,一點都不誇張。像這種邏輯很複雜的查詢,還可能涉及到依據不同條件執行不同sql的場景,我當然不會在高階語言中拼接原生sql了,因為那樣容易把自己搞暈哦。這個時候就要祭出最後的大殺技——儲存過程。
在CoffeeSQL中你可以這樣使用儲存過程:
1 using (ShowYouDbContext dbContext = new ShowYouDbContext()) 2 { 3 string strsp = "xxx_storeprocedureName"; 4 5 OracleParameter[] paras = new OracleParameter[] 6 { 7 new OracleParameter("V_DATE",OracleDbType.Varchar2,50), 8 new OracleParameter("V_SITE",OracleDbType.Varchar2,50), 9 new OracleParameter("V_SCREEN",OracleDbType.Varchar2,50), 10 new OracleParameter("V_SHIFT",OracleDbType.Varchar2,50), 11 new OracleParameter("V_CURSOR",OracleDbType.RefCursor)
13 }; 14 15 paras[0].Value = info.date; 16 paras[1].Value = info.site; 17 paras[2].Value = info.screenKey; 18 paras[3].Value = info.shift; 19 paras[4].Direction = ParameterDirection.Output;21 22 DataSet ds = dbContext.StoredProcedureQueryable(strsp, paras).ToDataSet(); 23 24 return ds; 25 }
這裡的用法並沒有做什麼包裝,值得一提的就是可以將查詢結果轉換為物件。當然,這是貫穿整個ORM框架的一大主要功能,無處不在。
5、資料庫連線管理,一主多從
現如今大多數的資料庫都不是單一部署的,比較流行的資料庫部署方式是“一主多從”的方式,即一臺資料庫作為寫入資料的資料庫(主庫),其他多臺資料庫作為讀取資料的資料庫(從庫)去同步主庫的資料,從而實現了讀寫分離的功能,降低了主庫的訪問壓力,大大提高了資料庫的訪問效能。當然,我們現在所討論的這個ORM框架就理所當然地要支援這種“一主多從”的資料庫部署方式的資料庫操作。
具體“一主多從”的資料庫連線配置方式如下:
1 public class ShowYouDbContext : OracleDbContext<ShowYouDbContext> 2 { 3 private static string writeConnStr = "xxxxxxx_1"; 4 private static string[] readConnStrs = new string[] { 5 6 "xxxxxxxxx_2", 7 "xxxxxxxxx_3", 8 "xxxxxxxxx_4", 9 "xxxxxxxxx_5", 10 "xxxxxxxxx_6" 11 }; 12 13 public ShowYouDbContext() : base(writeConnStr, readConnStrs) 14 { 15 } 16 }
對DBContext類物件進行構造時將資料庫連線字串當做構造引數傳入即可,第一條為主庫(寫資料庫)連線字串,以後的都為從庫(讀資料庫)連線字串。
6、實體資料驗證,作為資料格式規範的防線
實體資料驗證的概念很好理解:一個資料表中的每一個欄位都有其長度、大小等限制,那麼每一個與資料表對應的實體也應當有相應的欄位約束來作為資料格式規範的防線,讓持久化到資料表中的資料符合其所規定的的格式,也就相當於貨物在進入倉庫前做一個安全檢查。
我們可以像這樣去配置一個實體類的資料驗證規則:
1 [Table("B_DICTIONARY")] 2 public class Dictionary : EntityCommon 3 { 4 [Length(1,50)] 5 [Column] 6 public string Name { get; set; } 7 [Column] 8 public string Value { get; set; } 9 [Column] 10 public string Type { get; set; } 11 }
這個實體類中的Name欄位就標識了一個LengthAttribute標籤,規定了該欄位的長度範圍為1~50,如果不符合則會丟擲異常。當然,實體資料的驗證規則不止這一條,後期會根據需求再進行新增,或者使用者可以根據自己的需求進行擴充套件。
7、事務的操作形式,個人習慣,喜歡把transaction明確寫出來,不喜歡過度封裝
事務是資料庫系統中的重要概念,事務的特性是ACID(原子性、一致性、隔離性、永續性),當然這裡不會去討論事務的概念,我要展示的是在CoffeeSql中如何使用事務:
1 try 2 { 3 dbContext.DBTransaction.Begin(); 4 5 dbContext.Update<Machine_Match_Relation>(d => new { d.Del_Flag }, new Machine_Match_Relation { Del_Flag = 1 })
.Where(d => d.Screen_Machine_Id.Equals(displayDeviceId)).Done(); 6 7 foreach(string bindDeviceId in bindDeviceIds) 8 { 9 dbContext.Add(new Machine_Match_Relation 10 { 11 Id = Utils.GetGuidStr(), 12 Screen_Machine_Id = displayDeviceId, 13 Machine_Id = bindDeviceId, 14 Creater = updater 15 }); 16 } 17 18 dbContext.DBTransaction.Commit(); 19 } 20 catch(Exception ex) 21 { 22 dbContext.DBTransaction.Rollback(); 23 throw ex; 24 }
有人會說為什麼這裡不封裝一個方法,只要傳入業務操作程式碼的委託Action就行了,當然可以,但是說實在的,真沒必要,如果你喜歡就自己封裝去吧。
8、可適配擴充套件多款不同的資料庫
作為一個能跟的上潮流的ORM,當然得具備適配多種資料庫的特性了,Oracle、Mysql、SqlServer等等資料庫,想怎麼適配就怎麼適配。
1 //Mysql 2 public class WQSDbContext : MysqlDbContext<WQSDbContext> 3 { 4 public WQSDbContext() : base(mysqlConnStr) 5 { 6 this.OpenQueryCache = false; 7 this.Log = context => 8 { 9 Console.WriteLine($"sql:{context.SqlStatement}"); 10 Console.WriteLine($"time:{DateTime.Now}"); 11 }; 12 } 13 } 14 15 //Oracle 16 public class WQSDbContext : OracleDbContext<WQSDbContext> 17 { 18 public WQSDbContext() : base(oracleConnStr) 19 { 20 this.OpenQueryCache = false; 21 this.Log = context => 22 { 23 Console.WriteLine($"sql:{context.SqlStatement}"); 24 Console.WriteLine($"time:{DateTime.Now}"); 25 }; 26 } 27 }
目前CoffeeSQL實現了兩種資料庫的擴充套件,Oracle與Mysql。當然,如果你還想適配更多的資料庫的話,你也可以嘗試擴充套件,只不過我目前用到的這兩種資料庫。
9、快取功能,提高效能
還記得當初一次校招的面試,面試官問我:“你覺得ORM的速度快還是Ado.net的速度快?”
我傻傻地脫口而出:“那當然是Ado.net更快了,相比於ORM,它少了linq語句到sql的轉換步驟,而且ORM還比直接使用Ado.net多了查詢結果對映到物件的步驟。”
看面像和髮量就知道這個面試官 心(老)地(奸)善(巨)良(滑),他和我對視了3秒,然後說:
“好,今天就到這,回去等通知吧!”
等通知的後果那也就顯而易見了......
直到後來我深入地瞭解了ORM框架的原理才知道,那個問題並不是我回答的一句話那麼簡單,因為我忽略了ORM快取。
CoffeeSql中也會有ORM快取,ORM的快取分為表快取(二級快取)與sql語句快取(一級快取):表快取一般用於資料量較小且經常會進行查詢操作的表,會將整個表的資料快取到快取介質中;sql語句快取可以適用各種型別的表,它是以sql查詢語句作為快取鍵的。
當然,你如果並不想要知道太多其中的細節,在使用時你只需要像這樣簡單的配置:
(ORM快取開關)
1 public class WQSDbContext : OracleDbContext<WQSDbContext> 2 { 3 public WQSDbContext() : base(oracleConnStr) 4 { 5 this.OpenTableCache = true; 6 this.OpenQueryCache = true; 7 } 8 }
(表快取的實體配置)
1 [TableCaching] 2 [Table("B_DICTIONARY")] 3 public class Dictionary : EntityCommon 4 { 5 [Column] 6 public string Name { get; set; } 7 [Column] 8 public string Value { get; set; } 9 [Column] 10 public string Type { get; set; } 11 }
如果在實體類上標記TableCachingAttribute而且開啟了表快取,那麼就會對當前的資料表進行全表資料的快取。
要注意,表快取一般只用在小資料量且查詢頻繁的表中。因為表快取功能啟用後會事先去將全表的資料掃描然後儲存到本地快取,直接在快取中進行表資料的查詢操作,所以如果你將那種資料量很大且查詢並不是很頻繁的表開啟了表快取,那你可以想象一下這個肯定會是一個得不償失的行為。
以上的程式碼操作是我當初對CoffeeSQL的預想,當然,現在都成為了現實。以上的內容實際上就相當於CoffeeSQL的操作手冊,因為上面的程式碼完全是按照實際的CoffeeSQL的框架操作來進行展示的。
有關於CoffeeSQL更詳細的使用細節我會在原始碼的測試程式碼中給出,觀眾們可以移步去看原始碼:
https://gitee.com/xiaosen123/CoffeeSqlORM
本文為作者原創,轉載請註明出處:https://www.cnblogs.com/MaMaNongNong/p/12896757.html