[開源].NET數據庫訪問框架Chloe.ORM

扯淡

13年畢業之際,進入第一家公司實習,接觸了 EntityFramework,當時就以爲這東西太牛了,訪問數據庫均可以作得這麼輕鬆、優雅!畢竟那時還年輕,沒見過世面。工做以前爲了拿個實習機會混個工做證實,匆匆忙忙學了兩個月的 C#,就這樣,稀裏糊塗的作了程序員,今後走上了一條不歸路。那會也只知道 SqlHelper,DataTable。ORM?過高大上,沒據說過。雖然在第一家公司只呆了兩個月,但讓我認識了 EntityFramework,今後也走上了 ORM 的不歸路...純純的實體,增改刪超級簡單,查詢如行雲流水,真心沒理由抗拒!以致於後來進入第二家公司作開發極其不適應,由於他們沒用 EF,也不用類 linq 的 ORM,他們有本身數據庫訪問框架。那套東西實體設計複雜,支持的功能少,查詢條件還依賴字符串,開發容錯率過低,DB操做入口接口設計也很重,裏面方法不下60個,看心涼,用心累!那時,好懷念 EF~在新公司工做的時間內,來回都是增改頁面,作增刪查改,修復BUG,多少有點枯燥乏味,漸漸感受編碼能力提高太慢。同時鑑於用公司的 ORM 也不是很順手,因而,萌生了本身寫 ORM 的念頭,再而後...Chloe.ORM 面世了~git

導航

Chloe.ORM

Chloe 查詢接口設計借(zhao)鑑(ban) linq,但不支持 linq。開發以前,我給個人 ORM 查詢條件接口定義必定要支持lambda表達式(潮流、趨勢,在這不討論表達式樹的性能)。開發之初,也有本身設計過查詢接口,想了一套又一套,始終沒 linq 設計的接口方便,後來,不想了,直接抄 linq,不解釋!前人如此偉大設計,不用真對不起他們,我要站在他們的肩膀上!程序員

先看下 IDbContext 接口:github

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);
}

Chloe 操做入口是 IDbContext。IDbContext 僅有兩個 Query、兩個 Insert、兩個 Update 、兩個 Delete 和一個 TrackEntity 方法,以及一個 CurrentSession 的屬性,設計很簡單,但絕對能知足81%的需求(多一點知足,多一分熱愛)!
這篇文章,主要介紹 Query 接口使用。
sql

事前準備

實體:數據庫

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>();

基本查詢

IQuery<User> q = context.Query<User>();
q.Where(a => a.Id > 0).FirstOrDefault();
q.Where(a => a.Id > 0).ToList();
q.Where(a => a.Id > 0).OrderBy(a => a.Age).ToList();
q.Where(a => a.Id > 0).Take(999).OrderBy(a => a.Age).ToList();

//分頁。避免生成的 sql 語句太長佔篇幅,只選取 Id 和 Name 兩個字段
q.Where(a => a.Id > 0).OrderBy(a => a.Age).ThenByDesc(a => a.Id).Select(a => new { a.Id, a.Name }).Skip(1).Take(999).ToList();
/*
 * SELECT TOP (999) [T].[Id] AS [Id],[T].[Name] AS [Name] FROM (SELECT [Users].[Id] AS [Id],[Users].[Name] AS [Name],ROW_NUMBER() OVER(ORDER BY [Users].[Age] ASC,[Users].[Id] DESC) AS [ROW_NUMBER_0] FROM [Users] AS [Users] WHERE [Users].[Id] > 0) AS [T] WHERE [T].[ROW_NUMBER_0] > 1
 */

//若是須要多個條件的話
q.Where(a => a.Id > 0).Where(a => a.Name.Contains("lu")).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] FROM [Users] AS [Users] WHERE ([Users].[Id] > 0 AND [Users].[Name] LIKE '%' + N'lu' + '%')
 */

//選取指定字段
q.Select(a => new { a.Id, a.Name, a.Age }).ToList();
//或者
q.Select(a => new User() { Id = a.Id, Name = a.Name, Age = a.Age }).ToList();
/*
 * SELECT [Users].[Id] AS [Id],[Users].[Name] AS [Name],[Users].[Age] AS [Age] FROM [Users] AS [Users]
 */

鏈接查詢

創建鏈接:數據結構

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);

只獲取 UserId,CityName,ProvinceName:app

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 同樣,就不一一介紹了。

聚合函數

IQuery<User> q = context.Query<User>();

q.Select(a => DbFunctions.Count()).First();
/*
 * SELECT TOP (1) COUNT(1) AS [C] FROM [Users] AS [Users]
 */

q.Select(a => new { Count = DbFunctions.Count(), LongCount = DbFunctions.LongCount(), Sum = DbFunctions.Sum(a.Age), Max = DbFunctions.Max(a.Age), Min = DbFunctions.Min(a.Age), Average = DbFunctions.Average(a.Age) }).First();
/*
 * SELECT TOP (1) COUNT(1) AS [Count],COUNT_BIG(1) AS [LongCount],SUM([Users].[Age]) AS [Sum],MAX([Users].[Age]) AS [Max],MIN([Users].[Age]) AS [Min],CAST(AVG([Users].[Age]) AS FLOAT) AS [Average] FROM [Users] AS [Users]
 */

var count = q.Count();
/*
 * SELECT COUNT(1) AS [C] FROM [Users] AS [Users]
 */

var longCount = q.LongCount();
/*
 * SELECT COUNT_BIG(1) AS [C] FROM [Users] AS [Users]
 */

var sum = q.Sum(a => a.Age);
/*
 * SELECT SUM([Users].[Age]) AS [C] FROM [Users] AS [Users]
 */

var max = q.Max(a => a.Age);
/*
 * SELECT MAX([Users].[Age]) AS [C] FROM [Users] AS [Users]
 */

var min = q.Min(a => a.Age);
/*
 * SELECT MIN([Users].[Age]) AS [C] FROM [Users] AS [Users]
 */

var avg = q.Average(a => a.Age);
/*
 * SELECT CAST(AVG([Users].[Age]) AS FLOAT) AS [C] FROM [Users] AS [Users]
 */

分組查詢

IQuery<User> q = context.Query<User>();

IGroupingQuery<User> g = q.Where(a => a.Id > 0).GroupBy(a => a.Age);
g = g.Having(a => a.Age > 1 && DbFunctions.Count() > 0);

g.Select(a => new { a.Age, Count = DbFunctions.Count(), Sum = DbFunctions.Sum(a.Age), Max = DbFunctions.Max(a.Age), Min = DbFunctions.Min(a.Age), Avg = DbFunctions.Average(a.Age) }).ToList();
/*
 * SELECT [Users].[Age] AS [Age],COUNT(1) AS [Count],SUM([Users].[Age]) AS [Sum],MAX([Users].[Age]) AS [Max],MIN([Users].[Age]) AS [Min],CAST(AVG([Users].[Age]) AS FLOAT) AS [Avg] FROM [Users] AS [Users] WHERE [Users].[Id] > 0 GROUP BY [Users].[Age] HAVING ([Users].[Age] > 1 AND COUNT(1) > 0)
 */

SqlQuery

上面是純面向對象的方式查詢。鏈接查詢、聚合查詢、分組查詢如此輕鬆,有沒有以爲很方便?固然,始終和 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]
 */

徹底能夠用面向對象的方式就能夠實現,怎麼樣?很實用吧,免去拼 sql,讓更多的時間去作業務開發!

更多的用法還有待挖掘。

支持的lambda

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 Code

經常使用的函數

  IQuery<User> q = context.Query<User>();

  var space = new char[] { ' ' };

  DateTime startTime = DateTime.Now;
  DateTime endTime = DateTime.Now.AddDays(1);
  q.Select(a => new
{
    Id = a.Id,

    String_Length = (int?)a.Name.Length,//LEN([Users].[Name])
    Substring = a.Name.Substring(0),//SUBSTRING([Users].[Name],0 + 1,LEN([Users].[Name]))
    Substring1 = a.Name.Substring(1),//SUBSTRING([Users].[Name],1 + 1,LEN([Users].[Name]))
    Substring1_2 = a.Name.Substring(1, 2),//SUBSTRING([Users].[Name],1 + 1,2)
    ToLower = a.Name.ToLower(),//LOWER([Users].[Name])
    ToUpper = a.Name.ToUpper(),//UPPER([Users].[Name])
    IsNullOrEmpty = string.IsNullOrEmpty(a.Name),//太長,不貼了
    Contains = (bool?)a.Name.Contains("s"),//太長,略
    Trim = a.Name.Trim(),//RTRIM(LTRIM([Users].[Name]))
    TrimStart = a.Name.TrimStart(space),//LTRIM([Users].[Name])
    TrimEnd = a.Name.TrimEnd(space),//RTRIM([Users].[Name])
    StartsWith = (bool?)a.Name.StartsWith("s"),//太長,略
    EndsWith = (bool?)a.Name.EndsWith("s"),//太長,略

    SubtractTotalDays = endTime.Subtract(startTime).TotalDays,//CAST(DATEDIFF(DAY,@P_0,@P_1)
    SubtractTotalHours = endTime.Subtract(startTime).TotalHours,//CAST(DATEDIFF(HOUR,@P_0,@P_1)
    SubtractTotalMinutes = endTime.Subtract(startTime).TotalMinutes,//CAST(DATEDIFF(MINUTE,@P_0,@P_1)
    SubtractTotalSeconds = endTime.Subtract(startTime).TotalSeconds,//CAST(DATEDIFF(SECOND,@P_0,@P_1)
    SubtractTotalMilliseconds = endTime.Subtract(startTime).TotalMilliseconds,//CAST(DATEDIFF(MILLISECOND,@P_0,@P_1)

    Now = DateTime.Now,//GETDATE()
    UtcNow = DateTime.UtcNow,//GETUTCDATE()
    Today = DateTime.Today,//CAST(GETDATE() AS DATE)
    Date = DateTime.Now.Date,//CAST(GETDATE() AS DATE)
    Year = DateTime.Now.Year,//DATEPART(YEAR,GETDATE())
    Month = DateTime.Now.Month,//DATEPART(MONTH,GETDATE())
    Day = DateTime.Now.Day,//DATEPART(DAY,GETDATE())
    Hour = DateTime.Now.Hour,//DATEPART(HOUR,GETDATE())
    Minute = DateTime.Now.Minute,//DATEPART(MINUTE,GETDATE())
    Second = DateTime.Now.Second,//DATEPART(SECOND,GETDATE())
    Millisecond = DateTime.Now.Millisecond,//DATEPART(MILLISECOND,GETDATE())
    DayOfWeek = DateTime.Now.DayOfWeek,//(DATEPART(WEEKDAY,GETDATE()) - 1)

    Int_Parse = int.Parse("1"),//CAST(N'1' AS INT)
    Int16_Parse = Int16.Parse("11"),//CAST(N'11' AS SMALLINT)
    Long_Parse = long.Parse("2"),//CAST(N'2' AS BIGINT)
    Double_Parse = double.Parse("3"),//CAST(N'3' AS FLOAT)
    Float_Parse = float.Parse("4"),//CAST(N'4' AS REAL)
    Decimal_Parse = decimal.Parse("5"),//CAST(N'5' AS DECIMAL)
    Guid_Parse = Guid.Parse("D544BC4C-739E-4CD3-A3D3-7BF803FCE179"),//CAST(N'xxx' AS UNIQUEIDENTIFIER) AS [Guid_Parse]

    Bool_Parse = bool.Parse("1"),//CASE WHEN CAST(N'1' AS BIT) = CAST(1 AS BIT) THEN CAST(1 AS BIT) WHEN NOT (CAST(N'1' AS BIT) = CAST(1 AS BIT)) THEN CAST(0 AS BIT) ELSE NULL END AS [Bool_Parse]
    DateTime_Parse = DateTime.Parse("1992-1-16"),//CAST(N'1992-1-16' AS DATETIME) AS [DateTime_Parse]

    B = a.Age == null ? false : a.Age > 1,
}).ToList();
View Code

Chloe 的查詢,基本就這些用法。由於查詢接口直接借鑑 linq,因此,看起來就好像在介紹 linq 同樣,抱歉- -。也正由於這點,以前我把項目中的 EF 替換成 Chloe 的時候,由於我我的不怎麼用 linq 的 from in select 那種語法,因此,替換的時候幾乎不用改什麼代碼,就能夠成功編譯運行。EF 對實體間的關係處理得很是好,如一對多,一對一導航,Chloe 倒沒那麼強大。就目前的 Chloe 的 Query 接口,基本能夠知足大部分查詢需求了。

如今市面上各類ORM,層出不窮,有人可能會問 LZ 爲何還要重複造輪子?

  1. 這確實是一個ORM齊放的年代,各色各樣,千奇百怪的都有。但讓人滿意的框架(EF除外,EF在我心中是神同樣的存在)少之又少。作得不錯的,也總有些方面不足,偏偏卻由於一些小小的不足讓我止步,如實體複雜,不支持 lambda,支持lambda的但支持的寫法又很少,鏈接查詢不是很友好、便捷等等,都怪我太挑剔,抱歉。
  2. 文章開頭也說過,增刪查改,煩了。想用業餘時間作點有意思的東西,提高本身編碼能力的同時也能夠學到更多知識。由於寫了這個框架,我對面對對象的理解更加深入了,若是不嘗試的話,我估計我在程序員職業生涯內連個抽象類、接口都不會設計,更別說會什麼設計模式,面對對象編程原則了。之因此選擇作 ORM,由於 ORM 很貼切咱們平常開發,只要涉及數據庫,就能夠用到!
  3. 若是上面兩點還不足以讓您明白我爲何要造輪子,那最後我要告訴您的是:我是一枚任性的程序員,我就是要造輪子!

laida_thumb

結語

Chloe.ORM 徹底開源,遵循 Apache2.0 協議,託管於GitHub,供大夥學習參考,若是能參與開發與完善 Chloe 那再好不過了,項目地址:https://github.com/shuxinqin/Chloe。感興趣或以爲不錯的望賞個star,不勝感激!

若能順手點個贊,更加感謝!

相關文章
相關標籤/搜索