.NET高性能框架Chloe.ORM-完美支持MySql

时间:2022-01-01 02:00:13

扯淡

这是一款轻量、高效的.NET C#数据库访问框架(ORM)。查询接口借鉴 Linq(但不支持 Linq)。借助 lambda 表达式,可以完全用面向对象的方式就能轻松执行多表连接查询、分组查询、聚合查询、插入数据、批量删除和更新等操作。

支持主流数据库,似乎成了 ORM 的标配,Chloe 正在往这方向发展。Chloe 成型之初只支持 SqlServer,在很长的一段时间内,一直只是在维稳。经过公司项目中实战了一段时间,目前框架架构和功能都已经稳定,该支持的都已支持。因此,继上周做了性能测试后,花了点时间学习了下 MySql,然后花了些时间做了 MySql 的 Provider,现已支持 MySql 数据库。

导航

Chloe.ORM

事前准备

实体:

public enum Gender
{
    Man = 1,
    Woman
}

[Table("Users")]
public class User
{
    [Column(IsPrimaryKey = true)]
    [AutoIncrement]
    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; }
}

建个 MySqlConnectionFactory 类,实现 IDbConnectionFactory 接口:

public class MySqlConnectionFactory : IDbConnectionFactory
{
    string _connString = null;
    public MySqlConnectionFactory(string connString)
    {
        this._connString = connString;
    }
    public IDbConnection CreateConnection()
    {
        MySqlConnection conn = new MySqlConnection(this._connString);
        return conn;
    }
}

创建一个 DbContext:

MySqlContext context = new MySqlContext(new MySqlConnectionFactory(DbHelper.ConnectionString));

再创建一个 IQuery<T>:

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

查询数据

基本查询

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

q.Where(a => a.Id == 1).FirstOrDefault();
/*
 * 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` = 1 LIMIT 0,1
 */

//可以选取指定的字段
q.Where(a => a.Id == 1).Select(a => new { a.Id, a.Name }).FirstOrDefault();
/*
 * SELECT `Users`.`Id` AS `Id`,`Users`.`Name` AS `Name` FROM `Users` AS `Users` WHERE `Users`.`Id` = 1 LIMIT 0,1
 */

//分页
q.Where(a => a.Id > 0).OrderBy(a => a.Age).Skip(1).Take(999).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 ORDER BY `Users`.`Age` ASC LIMIT 1,999
 */

连接查询

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

//查出一个用户及其隶属的城市和省份的所有信息
var view = user_city_province.Select((user, city, province) => new { User = user, City = city, Province = province }).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
 */

//也可以只获取指定的字段信息:UserId,UserName,CityName,ProvinceName
user_city_province.Select((user, city, province) => new { UserId = user.Id, UserName = user.Name, CityName = city.Name, ProvinceName = province.Name }).Where(a => a.UserId == 1).ToList();
/*
 * SELECT `Users`.`Id` AS `UserId`,`Users`.`Name` AS `UserName`,`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
 */

聚合函数

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

q.Select(a => AggregateFunctions.Count()).First();
/*
 * SELECT COUNT(1) AS `C` FROM `Users` AS `Users` LIMIT 0,1
 */

q.Select(a => new { Count = AggregateFunctions.Count(), LongCount = AggregateFunctions.LongCount(), Sum = AggregateFunctions.Sum(a.Age), Max = AggregateFunctions.Max(a.Age), Min = AggregateFunctions.Min(a.Age), Average = AggregateFunctions.Average(a.Age) }).First();
/*
 * SELECT COUNT(1) AS `Count`,COUNT(1) AS `LongCount`,SUM(`Users`.`Age`) AS `Sum`,MAX(`Users`.`Age`) AS `Max`,MIN(`Users`.`Age`) AS `Min`,AVG(`Users`.`Age`) AS `Average` FROM `Users` AS `Users` LIMIT 0,1
 */

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

var longCount = q.LongCount();
/*
 * SELECT COUNT(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 AVG(`Users`.`Age`) 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 && AggregateFunctions.Count() > 0);

g.Select(a => new { a.Age, Count = AggregateFunctions.Count(), Sum = AggregateFunctions.Sum(a.Age), Max = AggregateFunctions.Max(a.Age), Min = AggregateFunctions.Min(a.Age), Avg = AggregateFunctions.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`,AVG(`Users`.`Age`) AS `Avg` FROM `Users` AS `Users` WHERE `Users`.`Id` > 0 GROUP BY `Users`.`Age` HAVING (`Users`.`Age` > 1 AND COUNT(1) > 0)
 */

插入数据

方式1

以 lambda 表达式树的方式插入:

//返回主键 Id
int id = (int)context.Insert<User>(() => new User() { Name = "lu", Age = 18, Gender = Gender.Man, CityId = 1, OpTime = DateTime.Now });
/*
 * INSERT INTO `Users`(`Name`,`Age`,`Gender`,`CityId`,`OpTime`) VALUES(N'lu',18,1,1,NOW());SELECT @@IDENTITY
 */

方式2

以实体的方式插入:

User user = new User();
user.Name = "lu";
user.Age = 18;
user.Gender = Gender.Man;
user.CityId = 1;
user.OpTime = DateTime.Now;

//会自动将自增 Id 设置到 user 的 Id 属性上
user = context.Insert(user);
/*
 * String ?P_0 = 'lu';
   Gender ?P_1 = Man;
   Int32 ?P_2 = 18;
   Int32 ?P_3 = 1;
   DateTime ?P_4 = '2016/7/24 21:33:58';
   INSERT INTO `Users`(`Name`,`Gender`,`Age`,`CityId`,`OpTime`) VALUES(?P_0,?P_1,?P_2,?P_3,?P_4);SELECT @@IDENTITY
 */

更新数据

方式1

以 lambda 表达式树的方式更新:

context.Update<User>(a => new User() { Name = a.Name, Age = a.Age + 100, Gender = Gender.Man, OpTime = DateTime.Now }, a => a.Id == 1);
/*
 * UPDATE `Users` SET `Name`=`Users`.`Name`,`Age`=(`Users`.`Age` + 100),`Gender`=1,`OpTime`=NOW() WHERE `Users`.`Id` = 1
 */

//批量更新
//给所有女性年轻 10 岁
context.Update<User>(a => new User() { Age = a.Age - 10, OpTime = DateTime.Now }, a => a.Gender == Gender.Woman);
/*
 * UPDATE `Users` SET `Age`=(`Users`.`Age` - 10),`OpTime`=NOW() WHERE `Users`.`Gender` = 2
 */

方式2

以实体的方式更新:

User user = new User();
user.Id = 1;
user.Name = "lu";
user.Age = 28;
user.Gender = Gender.Man;
user.OpTime = DateTime.Now;

context.Update(user); //会更新所有映射的字段
/*
 * String ?P_0 = 'lu';
   Gender ?P_1 = Man;
   Int32 ?P_2 = 28;
   Nullable<Int32> ?P_3 = NULL;
   DateTime ?P_4 = '2016/7/24 21:35:14';
   Int32 ?P_5 = 1;
   UPDATE `Users` SET `Name`=?P_0,`Gender`=?P_1,`Age`=?P_2,`CityId`=?P_3,`OpTime`=?P_4 WHERE `Users`.`Id` = ?P_5
 */

/*
 * 支持只更新属性值已变的属性
 */

context.TrackEntity(user);//在上下文中跟踪实体
user.Name = user.Name + "";
context.Update(user);//这时只会更新被修改的字段
/*
 * String ?P_0 = 'lu1';
   Int32 ?P_1 = 1;
   UPDATE `Users` SET `Name`=?P_0 WHERE `Users`.`Id` = ?P_1
 */

删除数据

方式1

以 lambda 表达式树的方式删除:

context.Delete<User>(a => a.Id == 1);
/*
 * DELETE `Users` FROM `Users` WHERE `Users`.`Id` = 1
 */

//批量删除
//删除所有不男不女的用户
context.Delete<User>(a => a.Gender == null);
/*
 * DELETE `Users` FROM `Users` WHERE `Users`.`Gender` IS NULL
 */

方式2

以实体的方式删除:

User user = new User();
user.Id = 1;
context.Delete(user);
/*
 * Int32 ?P_0 = 1;
   DELETE `Users` FROM `Users` WHERE `Users`.`Id` = ?P_0
 */

支持函数

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

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

DateTime startTime = DateTime.Now;
DateTime endTime = DateTime.Now.AddDays(1);

var ret = q.Select(a => new
{
    Id = a.Id,

    String_Length = (int?)a.Name.Length,//LENGTH(`Users`.`Name`)
    Substring = a.Name.Substring(0),//SUBSTRING(`Users`.`Name`,0 + 1,LENGTH(`Users`.`Name`))
    Substring1 = a.Name.Substring(1),//SUBSTRING(`Users`.`Name`,1 + 1,LENGTH(`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),//CASE WHEN (`Users`.`Name` IS NULL OR `Users`.`Name` = N'') THEN 1 ELSE 0 END = 1
    Contains = (bool?)a.Name.Contains("s"),//`Users`.`Name` LIKE CONCAT('%',N's','%')
    Trim = a.Name.Trim(),//TRIM(`Users`.`Name`)
    TrimStart = a.Name.TrimStart(space),//LTRIM(`Users`.`Name`)
    TrimEnd = a.Name.TrimEnd(space),//RTRIM(`Users`.`Name`)
    StartsWith = (bool?)a.Name.StartsWith("s"),//`Users`.`Name` LIKE CONCAT(N's','%')
    EndsWith = (bool?)a.Name.EndsWith("s"),//`Users`.`Name` LIKE CONCAT('%',N's')

    DiffYears = DbFunctions.DiffYears(startTime, endTime),//TIMESTAMPDIFF(YEAR,?P_0,?P_1)
    DiffMonths = DbFunctions.DiffMonths(startTime, endTime),//TIMESTAMPDIFF(MONTH,?P_0,?P_1)
    DiffDays = DbFunctions.DiffDays(startTime, endTime),//TIMESTAMPDIFF(DAY,?P_0,?P_1)
    DiffHours = DbFunctions.DiffHours(startTime, endTime),//TIMESTAMPDIFF(HOUR,?P_0,?P_1)
    DiffMinutes = DbFunctions.DiffMinutes(startTime, endTime),//TIMESTAMPDIFF(MINUTE,?P_0,?P_1)
    DiffSeconds = DbFunctions.DiffSeconds(startTime, endTime),//TIMESTAMPDIFF(SECOND,?P_0,?P_1)
    //DiffMilliseconds = DbFunctions.DiffMilliseconds(startTime, endTime),//MySql 不支持 Millisecond
    //DiffMicroseconds = DbFunctions.DiffMicroseconds(startTime, endTime),//ex

    Now = DateTime.Now,//NOW()
    UtcNow = DateTime.UtcNow,//UTC_TIMESTAMP()
    Today = DateTime.Today,//CURDATE()
    Date = DateTime.Now.Date,//CURDATE()
    Year = DateTime.Now.Year,//YEAR(NOW())
    Month = DateTime.Now.Month,//MONTH(NOW())
    Day = DateTime.Now.Day,//DAY(NOW())
    Hour = DateTime.Now.Hour,//HOUR(NOW())
    Minute = DateTime.Now.Minute,//MINUTE(NOW())
    Second = DateTime.Now.Second,//SECOND(NOW())
    Millisecond = DateTime.Now.Millisecond,//?P_2 AS `Millisecond`
    DayOfWeek = DateTime.Now.DayOfWeek,//(DAYOFWEEK(NOW()) - 1)

    //Byte_Parse = byte.Parse("1"),//不支持
    Int_Parse = "),//CAST(N'1' AS SIGNED)
    Int16_Parse = Int16.Parse(""),//CAST(N'11' AS SIGNED)
    Long_Parse = "),//CAST(N'2' AS SIGNED)
    //Double_Parse = double.Parse("3"),//N'3' 不支持,否则可能会成为BUG
    //Float_Parse = float.Parse("4"),//N'4' 不支持,否则可能会成为BUG
    //Decimal_Parse = decimal.Parse("5"),//不支持
    Guid_Parse = Guid.Parse("D544BC4C-739E-4CD3-A3D3-7BF803FCE179"),//N'D544BC4C-739E-4CD3-A3D3-7BF803FCE179'

    Bool_Parse = "),//CAST(N'1' AS SIGNED)
    DateTime_Parse = DateTime.Parse("2014-1-1"),//CAST(N'2014-1-1' AS DATETIME)
}).ToList();

坎坎坷坷

MySql 和 SqlServer 同为关系型数据库,两者大同小异。相对来说,MySql 稍微简单了些。比如在 SqlServer 里,select 1>0 这种语法是不支持的,但在 MySql 里却可以,对于 >、<、= 等等的一些比较运算符,是可以作为有返回值结果返回(1或0),这给我开发 MySql Provider 方便了许多,因此,MySql Provider 较 SqlServer Provider 的代码量也少了些。但两者的一些差异也给我带来不少麻烦!

因为本来就对 MySql 零认知,即使是一点点的不同,也挺折腾的。比如,MySql 不支持 Full Join,我完全不知道。开发调试的时候报错,一直以为是语法问题,检查了一遍又一遍的语法,感觉就是没错啊,但就是没法执行,始终不知道是怎么回事!将 Full Join 改成 Inner Join 或 Left Join 又可以执行了,愁死我了,因为压根没想过也觉得不可能是因为 MySql 不支持 Full Join 的原因,折腾了我半天!

还有,因为 Chloe 内部对数据类型要求很严谨,很依赖数据的 CAST 类型转换语法,在 SqlServer 里,CAST 的目标类型只要是 SqlServer 支持的类型都可以。但到了 MySql 就不一样了,转换的目标类型就限定几个(CHAR、DATE、TIME、DATETIME、DECIMAL、SIGNED 和 UNSIGNED),如果需要将一个 Int 类型转换成 Double 类型怎么办?貌似还真没办法,我也是没辙...因为,如果一个数据库字段是 Int 类型,在 SqlServer 的 DataReader 里调用不是 GetInt 方法,而是如 GetDouble、GetInt64 这类的强类型方法会报错,不允许这样调用。我的思维定型在 SqlServer 上了,所以在 MySql 的 CAST 转换上折腾了好久!后来不知道那根筋膨胀,突然想到在 MySql 的 DataReader 上会不会就可以调用 GetDouble、GetInt64 呢?尝试了下,还真可以,豁然开朗。数据库类型是 Int 类型,可以用 GetDouble、GetInt64 获取值,这真的万万想不到- -。不过 MySql 的 DataReader 为什么可以这样玩呢?好奇的我想知道为什么,网上应该有 MySql.Data.dll 的源码吧!于是上 GitHub 搜罗了一番,还真有!不得不佩服开源力量的强大!看了下它对强类型方法实现,大概都是类似下面这样:

public override Int64 GetInt64(int i)
{
    IMySqlValue v = GetFieldValue(i, true);
    if (v is MySqlInt64)
        return ((MySqlInt64)v).Value;

    return (Int64)ChangeType(v, i, typeof(Int64));
}

看到了吗?原来它做了个判断,如果获取的类型不是 Int64 类型数据,它会自己帮我们做类型转换。我估计是因为 MySql CAST 支持转换的目标类型有限,DataReader 才不得已做了这么一个措施!看起来很人性化,但从另一方面来说有点不严谨(怪我,被 SqlServer 影响太深)!

这些小问题看起来不值一提,但确实花了好多时间解决它!细节,真是搞死咱这些小白了!

结语

由于,从一开始就给 Chloe.ORM 定了要支持多数据库的目标,因此,在设计框架的时候,把 SqlGenerator 给抽象了出来,所以,这次做 MySql 的 Provider 没动任何 Chloe.dll 项目里的代码,只是增加了一个 Chloe.MySql.dll 扩展。学习 MySql 并深入理解 MySql 花了我挺长时间,但开发 Chloe.MySql.dll 却只用了两个多小时。由于本来就对 MySql 很陌生,在接下来的一段时间内,我会继续对 Chloe.MySql 做测试,保证“零BUG”。

Chloe.ORM 还小,要走的路还很长。近期貌似 .NET Core 很火,接下来的发展目标是支持 .NET Core。对 Chloe.ORM 项目感兴趣的同学,敬请期待!

Chloe.ORM 完全开源,遵循 Apache2.0 协议,托管于 GitHub,地址:https://github.com/shuxinqin/Chloe

相关介绍:http://www.cnblogs.com/so9527/p/5636216.html
性能测试:http://www.cnblogs.com/so9527/p/5674498.html