13年畢業之際,進入第一家公司實習,接觸了 EntityFramework,當時就覺得這東西太牛了,訪問數據庫都可以做得這麼輕松、優雅!畢竟那時還年輕,沒見過世面。工作之前為了拿個實習機會混個工作證明,匆匆忙忙學了兩個月的 C#,就這樣,稀裡糊塗的做了程序員,從此走上了一條不歸路。那會也只知道 SqlHelper,DataTable。ORM?太高大上,沒聽說過。雖然在第一家公司只呆了兩個月,但讓我認識了 EntityFramework,從此也走上了 ORM 的不歸路...純純的實體,增改刪超級簡單,查詢如行雲流水,真心沒理由抗拒!以至於後來進入第二家公司做開發極其不適應,因為他們沒用 EF,也不用類 linq 的 ORM,他們有自己數據庫訪問框架。那套東西實體設計復雜,支持的功能少,查詢條件還依賴字符串,開發容錯率太低,DB操作入口接口設計也很重,裡面方法不下60個,看心涼,用心累!那時,好懷念 EF~在新公司工作的時間內,來回都是增改頁面,做增刪查改,修復BUG,多少有點枯燥乏味,漸漸感覺編碼能力提升太慢。同時鑒於用公司的 ORM 也不是很順手,於是,萌生了自己寫 ORM 的念頭,再然後...Chloe.ORM 面世了~
Chloe 查詢接口設計借(zhao)鑒(ban) linq,但不支持 linq。開發之前,我給我的 ORM 查詢條件接口定義一定要支持lambda表達式(潮流、趨勢,在這不討論表達式樹的性能)。開發之初,也有自己設計過查詢接口,想了一套又一套,始終沒 linq 設計的接口方便,後來,不想了,直接抄 linq,不解釋!前人如此偉大設計,不用真對不起他們,我要站在他們的肩膀上!
先看下 IDbContext 接口:
public interface IDbContext : IDisposable { IDbSession CurrentSession { get; } IQuery<T> Query<T>() where T : new(); IEnumerable<T> SqlQuery<T>(string sql, params DbParam[] parameters) where T : new(); T Insert<T>(T entity); object Insert<T>(Expression<Func<T>> body); int Update<T>(T entity); int Update<T>(Expression<Func<T, T>> body, Expression<Func<T, bool>> condition); int Delete<T>(T entity); int Delete<T>(Expression<Func<T, bool>> condition); void TrackEntity(object entity); } View CodeChloe 操作入口是 IDbContext。IDbContext 僅有兩個 Query、兩個 Insert、兩個 Update 、兩個 Delete 和一個 TrackEntity 方法,以及一個 CurrentDbSession 的屬性,設計很簡單,但絕對能滿足81%的需求(多一點滿足,多一分熱愛)!
這篇文章,主要介紹 Query 接口使用。
實體:
public enum Gender { Man = 1, Woman } [TableAttribute("Users")] public class User { [Column(IsPrimaryKey = true)] [AutoIncrementAttribute] public int Id { get; set; } public string Name { get; set; } public Gender? Gender { get; set; } public int? Age { get; set; } public int? CityId { get; set; } public DateTime? OpTime { get; set; } } public class City { [Column(IsPrimaryKey = true)] public int Id { get; set; } public string Name { get; set; } public int ProvinceId { get; set; } } public class Province { [Column(IsPrimaryKey = true)] public int Id { get; set; } public string Name { get; set; } } View Code首先,創建一個 DbContext:
IDbContext context = new MsSqlContext(DbHelper.ConnectionString);
再創建一個 IQuery<T>:
IQuery<User> q = context.Query<User>();
建立連接:
MsSqlContext context = new MsSqlContext(DbHelper.ConnectionString); IQuery<User> users = context.Query<User>(); IQuery<City> cities = context.Query<City>(); IQuery<Province> provinces = context.Query<Province>(); IJoiningQuery<User, City> user_city = users.InnerJoin(cities, (user, city) => user.CityId == city.Id); IJoiningQuery<User, City, Province> user_city_province = user_city.InnerJoin(provinces, (user, city, province) => city.ProvinceId == province.Id); View Code只獲取 UserId,CityName,ProvinceName:
user_city_province.Select((user, city, province) => new { UserId = user.Id, CityName = city.Name, ProvinceName = province.Name }).Where(a => a.UserId == 1).ToList(); /* * SELECT [Users].[Id] AS [UserId],[City].[Name] AS [CityName],[Province].[Name] AS [ProvinceName] FROM [Users] AS [Users] INNER JOIN [City] AS [City] ON [Users].[CityId] = [City].[Id] INNER JOIN [Province] AS [Province] ON [City].[ProvinceId] = [Province].[Id] WHERE [Users].[Id] = 1 */
調用 Select 方法返回一個包含所有信息的 IQuery<T> 對象:
var view = user_city_province.Select((user, city, province) => new { User = user, City = city, Province = province });
查出一個用戶及其隸屬的城市和省份:
view.Where(a => a.User.Id == 1).ToList(); /* * SELECT [Users].[Id] AS [Id],[Users].[Name] AS [Name],[Users].[Gender] AS [Gender],[Users].[Age] AS [Age],[Users].[CityId] AS [CityId],[Users].[OpTime] AS [OpTime],[City].[Id] AS [Id0],[City].[Name] AS [Name0],[City].[ProvinceId] AS [ProvinceId],[Province].[Id] AS [Id1],[Province].[Name] AS [Name1] FROM [Users] AS [Users] INNER JOIN [City] AS [City] ON [Users].[CityId] = [City].[Id] INNER JOIN [Province] AS [Province] ON [City].[ProvinceId] = [Province].[Id] WHERE [Users].[Id] = 1 */
這時候也可以選取指定的字段:
view.Where(a => a.User.Id == 1).Select(a => new { UserId = a.User.Id, CityName = a.City.Name, ProvinceName = a.Province.Name }).ToList(); /* * SELECT [Users].[Id] AS [UserId],[City].[Name] AS [CityName],[Province].[Name] AS [ProvinceName] FROM [Users] AS [Users] INNER JOIN [City] AS [City] ON [Users].[CityId] = [City].[Id] INNER JOIN [Province] AS [Province] ON [City].[ProvinceId] = [Province].[Id] WHERE [Users].[Id] = 1 */
Chloe 也支持 Left Join、Right Join、Full Join連接,用法和 Inner Join 一樣,就不一一介紹了。
上面是純面向對象的方式查詢。連接查詢、聚合查詢、分組查詢如此輕松,有沒有覺得很方便?當然,始終和 linq 那種接近 sql 的 from v in q where v > 3 select v 寫法沒法比!同時,ORM始終是個工具,它並不是萬能的,對於一些復雜的語句,還是得需要手寫,因此,DbContext 也提供原生 sql 查詢接口
context.SqlQuery<User>("select Id,Name,Age from Users where Name=@name", DbParam.Create("@name", "lu")).ToList(); context.SqlQuery<int>("select Id from Users").ToList();
經測試,非 Debug 情況下,且都經過預熱後,相同的查詢在速度、性能上與 Dapper 相當,甚至比 Dapper 還快那麼一丟丟。
IQuery<T> 接口支持連接查詢、聚合查詢、分組查詢,這幾個接口配合使用可以減少很多我們開發中的煩惱。比如:
做數據庫開發,多表關聯的數據結構肯定不少,難免會有多表連接查詢,很多時候,為了方便查詢,一般我們都會建立視圖。在我看來視圖很煩,真的煩。
int 煩 = 0;
1.建視圖的時候,字段多的話,煩++,如果出現字段重名的情況,必須起別名,煩++。
2.視圖建立起來了以後,查詢是方便了,但後面維護就不那麼友好了,比如某個表字段名改了、增加一個字段、刪除一個字段等情況,得修改相應的視圖(1個或多個),煩++;同時又要去修改相映射的實體,煩++。總之,Console.Write("煩煩煩: " + 煩.ToString()); 對於我這種懶程序員,這簡直就是種煎熬!如果一套 ORM 支持連接查詢,在一定程度上可以減少在數據庫上建視圖數量,無形中省出好多時間。
為了讓 Chloe 支持連接查詢,費了我不少勁。連接查詢的好處可以看上面連接查詢部分。
比如,本文中的 User 表、City 表,他們的關系是一個 User 隸屬一個 City,一個 City 有多個用戶。假設,現在有需求要查出 City 的信息,同時也要把該 City 下用戶最小的年齡輸出,如果用原生 sql 寫的話大概是:
select City.*,T.MinAge from City left join (select CityId,Min(Users.Age) as MinAge from Users group by Users.CityId) as T on City.Id=T.CityId
雖然也不是很復雜。來看看 Chloe 如何實現:
IQuery<User> users = context.Query<User>(); IQuery<City> cities = context.Query<City>(); var gq = users.GroupBy(a => a.CityId).Select(a => new { a.CityId, MinAge = DbFunctions.Min(a.Age) }); cities.LeftJoin(gq, (city, g) => city.Id == g.CityId).Select((city, g) => new { City = city, MinAge = g.MinAge }).ToList(); /* * SELECT [T].[MinAge] AS [MinAge],[City].[Id] AS [Id],[City].[Name] AS [Name],[City].[ProvinceId] AS [ProvinceId] FROM [City] AS [City] LEFT JOIN (SELECT [Users].[CityId] AS [CityId],MIN([Users].[Age]) AS [MinAge] FROM [Users] AS [Users] GROUP BY [Users].[CityId]) AS [T] ON [City].[Id] = [T].[CityId] */ View Code完全可以用面向對象的方式就可以實現,怎麼樣?很實用吧,免去拼 sql,讓更多的時間去做業務開發!
更多的用法還有待挖掘。
Chloe 查詢條件依賴 lambda 表達式,從對 lambda 表達式樹零認知到完成對其解析這塊,花了我好多精力,費了好多神,掉了不少頭發。現在對謂語支持很豐富,可以說愛怎麼寫就怎麼寫~
IQuery<User> q = context.Query<User>(); List<int> ids = new List<int>(); ids.Add(1); ids.Add(2); ids.Add(2); string name = "lu"; string nullString = null; bool b = false; bool b1 = true; q.Where(a => true).ToList(); q.Where(a => a.Id == 1).ToList(); q.Where(a => a.Id == 1 || a.Id > 1).ToList(); q.Where(a => a.Id == 1 && a.Name == name && a.Name == nullString && a.Id == FeatureTest.ID).ToList(); q.Where(a => ids.Contains(a.Id)).ToList(); q.Where(a => !b == (a.Id > 0)).ToList(); q.Where(a => a.Id > 0).Where(a => a.Id == 1).ToList(); q.Where(a => !(a.Id > 10)).ToList(); q.Where(a => !(a.Name == name)).ToList(); q.Where(a => a.Name != name).ToList(); q.Where(a => a.Name == name).ToList(); q.Where(a => (a.Name == name) == (a.Id > 0)).ToList(); q.Where(a => a.Name == (a.Name ?? name)).ToList(); q.Where(a => (a.Age == null ? 0 : 1) == 1).ToList(); //運算操作符 q.Select(a => new { Add = 1 + 2, Subtract = 2 - 1, Multiply = 2 * 11, Divide = 4 / 2, And = true & false, IntAnd = 1 & 2, Or = true | false, IntOr = 3 | 1, }).ToList(); View CodeChloe 的查詢,基本就這些用法。因為查詢接口直接借鑒 linq,所以,看起來就好像在介紹 linq 一樣,抱歉- -。也正因為這點,之前我把項目中的 EF 替換成 Chloe 的時候,因為我個人不怎麼用 linq 的 from in select 那種語法,所以,替換的時候幾乎不用改什麼代碼,就可以成功編譯運行。EF 對實體間的關系處理得非常好,如一對多,一對一導航,Chloe 倒沒那麼強大。就目前的 Chloe 的 Query 接口,基本可以滿足大部分查詢需求了。
現在市面上各種ORM,層出不窮,有人可能會問 LZ 為什麼還要重復造輪子?
Chloe.ORM 完全開源,遵循 Apache2.0 協議,托管於GitHub,供大伙學習參考,如果能參與開發與完善 Chloe 那再好不過了,項目地址:https://github.com/shuxinqin/Chloe。感興趣或覺得不錯的望賞個star,不勝感激!
若能順手點個贊,更加感謝!