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