一, 简介
FreeSql 是 .NET 平台下的对象关系映射技术(O/RM), 支持 .NetCore 2.1+ 或 .NetFramework 4.0+ 或 Xamarin.
从 0.0.1 发布, 历时整整一年的迭代更新, 原计划元旦发布 1.0, 可能作者比较急提前了几天发布. 其实是元旦有其他事......
本文内容从简, 介绍项目的主要功能框架, 以及暂时能想到的可能比较有说服力的特性.
二, 项目统计
主仓库解决方案共计项目: 29 个
单元测试: 3510 个
Code Issues:168 个
文档 Wiki:43 个
- Stars:1140
- Forks:236
Commits:690 次
Nuget 主包下载量: 86,568 次
开源地址: https://github.com/2881099/FreeSql
三, 功能结构
支持 CodeFirst 迁移, 哪怕使用 Access 数据库也支持;
支持 DbFirst 从数据库导入实体类;
支持 深入的类型映射, 比如 pgsql 的数组类型;
支持 丰富的表达式函数, 以及灵活的自定义解析;
支持 导航属性一对多, 多对多贪婪加载, 以及延时加载;
支持 读写分离, 分表分库, 租户设计, 过滤器, 乐观锁, 悲观锁;
支持 MySQL/SqlServer/PostgreSQL/Oracle/SQLite / 达梦数据库 / Access;
四, CodeFirst/DbFirst
一切皆 CodeFirst, 所有功能都是由实体类型, 到表操作的过程. CodeFirst [自动迁移] 只需要一行代码:
- using FreeSql;
- static IFreeSql fsql = new FreeSqlBuilder()
- .UseConnectionString(DataType.SQLite,
- @"Data Source=|DataDirectory|\document.db;Pooling=true;Max Pool Size=10")
- .UseAutoSyncStructure(true) // 自动同步实体结构到数据库
- .Build();
在开发过程中, 表结构会自动创建, 或改变(不丢数据), 取决于实体类的变化.
CodeFirst 提供功能丰富的特性 ColumnAttribute, 定义实体与表间的映射, 并且支持 FluentApi 方式. 如果不喜欢 ColumnAttribute 这个名字, 还可以通过 AOP 设置换为 MyColumnAttribute.
- using FreeSql.DataAnnotations;
- class Song {
- [Column(IsIdentity = true)]
- public int Id { get; set; }
- public string Title { get; set; }
- public string Url { get; set; }
- public DateTime CreateTime { get; set; }
- }
DbFirst 数据表先行, 许多哥们使用动软, T4 模板生成实体类代码. 自已处理每种数据库的字段类型, 和 csharp 类型对应, 比较麻烦, 各大 ORM 可能还不通用.
我们提供命令行工具生成实体类, dotnet-tools, 对就是它.. 非常好用的工具, 没有之一.
C:\Users\28810>dotnet tool install -g freesql.generator
可使用以下命令调用工具: FreeSql.Generator
已成功安装工具 "freesql.generator"(版本 "1.0.0").
C:\Users\28810>FreeSql.Generator -help
它基于 Razor 模板生成, 支持自定义模板生成, 意味着它远不止可以生成实体类, 甚至是 IRepository 或者...
五, 导航属性
从一开始就着重导航对象的设计, 支持一对多, 多对多, 父子关系, 一对一, 多对一, 不夸张的说目前对导航属性处理最流弊, 最容易上手的 ORM. 多表查询的表达式使用非常便利, 如下:
- fsql.Select<Catetory>()
- .Where(a => a.Parent.Parent.Name == "粤语")
可以使用导航属性一直这样点下去...
级联保存, 级联查询功能也必不可少, 如下查询多对多:
- fsql.Select<Song>()
- .IncludeMany(a => a.Tags)
- .ToList();
上面的代码, 如果只返回 Tags 前 5 条记录, 也是支持的 .IncludeMany(a => a.Tags.Take(5))
对性能有追求, 还可以指定 Tags 只查询部分字段
关于 IncludeMany 不便再这过多展开介绍...(其实还有黑科技!)
哦, 还有 FreeSql.AdminLTE 扩展包, 它不属于主仓库项目, 最大化利用导航属性完成通用的 CURD 后台管理功能.
流弊哒哒~~~~
六, 仓储模式
仓储工作单元目前是当下的流行风, 在比较早的时候大约 0.2 版本发布了第一个仓储版本, 当时参考了大量的项目设计, 最终选用 abp vnext 的 IRepository 设计接口, 实现通用仓储类功能.
也就是说, 使用 FreeSql.Repository 你不必再自己写那些繁琐的 CURD 重复的仓储功能, 不用再头疼仓储类的接口方法定义. 定义标准比写代码难多了, abp vnext 的 IRepository 目前是见过最好的, 木有之一!!
仓储模式都在操作实体对象, 无论是更新还是删除, 都是传对象... 传传传...
问题 1, 传对象更新, 意味着更新所有字段?
不会的, 我们的仓储实现拥有状态管理机制, 从对象查询出来的时候已经记录了拍照, 当调用更新方法的时候会与之对比, 计算出变化的字段, 只更新变化的字段!
- var repo = fsql.GetRepository<Song>();
- var item = repo.Where(a => a.Id == 1).First();
- item.Title = "原谅我今天";
- repo.Update(item);
提示: 支持乐观锁, 悲观锁
问题 2, 状态管理是否影响性能?
不完全, 因为状态管理设计在仓储实现之上, 我们最原始的 IFreeSql 没有这个功能(仓储算是一种扩展包吧, 但是仓储又非常有效). 仓储即用即销毁, 擅用它的对比功能更新对象, 不滥用没有性能问题.
有了仓储怎么会没有 UnitOfWork 呢, UnitOfWork 目前以事务的方式做了默认实现, 并且它拥有实体变化跟踪记录.
七, 性能
1, 插入测试(52 个字段)
18W | 1W | 5K | 2K | 1K | 500 | 100 | 50 | |
---|---|---|---|---|---|---|---|---|
MySql 5.5 ExecuteAffrows | 55,497 | 4,953 | 2,304 | 2,554 | 1,516 | 1,572 | 265 | 184 |
SqlServer Express ExecuteAffrows | 402,355 | 24,847 | 11,465 | 4,971 | 2,437 | 915 | 138 | 88 |
SqlServer Express ExecuteSqlBulkCopy | 21,065 | 578 | 326 | 139 | 105 | 79 | 60 | 48 |
PostgreSQL 10 ExecuteAffrows | 46,756 | 3,294 | 2,269 | 1,019 | 374 | 209 | 51 | 37 |
PostgreSQL 10 ExecutePgCopy | 10,090 | 583 | 337 | 136 | 88 | 61 | 30 | 25 |
Oracle XE ExecuteAffrows | - | - | - | - | 24,528 | 10,648 | 571 | 200 |
Sqlite ExecuteAffrows | 28,554 | 1,149 | 701 | 327 | 155 | 91 | 44 | 35 |
测试结果, 是在相同操作系统下进行的, 并且都有预热
18W 解释: 插入 18 万行记录, 表格中的数字是执行时间(单位 ms)
Oracle 插入性能不用怀疑, 可能安装学生版限制较大
提醒: 开源数据库测试结果比较有意义, 商业数据库版本之间性能可能有较大差距
2, 插入测试(10 个字段)
18W | 1W | 5K | 2K | 1K | 500 | 100 | 50 | |
---|---|---|---|---|---|---|---|---|
MySql 5.5 ExecuteAffrows | 15,380 | 1,813 | 1,457 | 1,254 | 563 | 246 | 55 | 21 |
SqlServer Express ExecuteAffrows | 47,204 | 2,275 | 1,108 | 488 | 279 | 123 | 35 | 16 |
SqlServer Express ExecuteSqlBulkCopy | 4,248 | 127 | 71 | 30 | 48 | 14 | 11 | 10 |
PostgreSQL 10 ExecuteAffrows | 9,786 | 568 | 336 | 157 | 102 | 34 | 9 | 6 |
PostgreSQL 10 ExecutePgCopy | 4,081 | 167 | 93 | 39 | 21 | 12 | 4 | 2 |
Oracle XE ExecuteAffrows | - | - | - | - | 2,394 | 731 | 67 | 33 |
Sqlite ExecuteAffrows | 4,524 | 246 | 137 | 94 | 35 | 19 | 14 | 11 |
提示: 已经支持了 SqlServer 数据库的 SqlBulkCopy 功能, 以及 PostgreSQL 数据库的 Copy 功能
八, 拉姆达
非常特色的功能之一, 深入细化函数解析, 所支持的类型基本都可以使用对应的表达式函数, 例如 日期, 字符串, IN 查询, 数组 (PostgreSQL 的数组), 字典(PostgreSQL HStore) 等等.
1,In 查询
- var t1 = fsql.Select<T>()
- .Where(a => new[] { 1, 2, 3 }.Contains(a.Id))
- .ToSql();
- //SELECT .. FROM ..
- //WHERE (a.`Id` in (1,2,3))
已优化, 防止 where in 元素多过的 SQL 错误, 如:
[Err] ORA-01795: maximum number of expressions in a list a 1000
原来: where id in (1..1333)
现在: where id in (1..500) or id in (501..1000) or id in (1001..1333)
2,In 查询(多列)
- // 元组集合
- vae lst = new List<(Guid, DateTime)>();
- lst.Add((Guid.NewGuid(), DateTime.Now));
- lst.Add((Guid.NewGuid(), DateTime.Now));
- lst.Add((Guid.NewGuid(), DateTime.Now));
- fsql.Select<T>()
- .Where(a => lst.Contains(a.Id, a.ct1))
- .ToSql();
- //SELECT .. FROM ..
- //WHERE (a."Id" = '685ee1f6-bdf6-4719-a291-c709b8a1378f' AND a."ct1" = '2019-12-07 23:55:27' OR
- //a."Id" = '5ecd838a-06a0-4c81-be43-1e77633b7404' AND a."ct1" = '2019-12-07 23:55:27' OR
- //a."Id" = 'b8b366f3-1c03-4547-9c96-d362dd5cae6a' AND a."ct1" = '2019-12-07 23:55:27')
3, 自定义函数
默认已经支持了很丰富的函数解析, 如果不够再自己定义:
- [ExpressionCall]
- public static class DbFunc
- {
- // 必要定义 static + ThreadLocal
- static ThreadLocal<ExpressionCallContext> context = new ThreadLocal<ExpressionCallContext>();
- public static DateTime FormatDateTime(this DateTime that, string arg1)
- {
- var up = context.Value;
- if (up.DataType == FreeSql.DataType.SQLite) // 重写内容
- context.Value.Result = $"date_format({up.ParsedContent["that"]}, {up.ParsedContent["arg1"]})";
- return that;
- }
- }
- fsql.Select<T>().ToSql(a => a.CreateTime.FormatDateTime("yyyy-MM-dd"));
- //SELECT date_format(a."CreateTime", 'yyyy-MM-dd') as1
- //FROM "T" a
提示: SqlServer nvarchar/varchar 已兼容表达式解析, 分别解析为: N''和'', 优化索引执行计划
九, 骚操作
1, 代码注释 -> 迁移到数据库
CodeFirst 支持将 c# 代码内的注释, 迁移至数据库的备注. 先决条件:
实体类所在程序集, 需要开启 xml 文档功能;
xml 文件必须与程序集同目录, 且文件名: xxx.dll -> xxx.xml;
2,NoneParameter
可以设置不使用 参数化 执行 SQL 命令, 方便开发调试, 区别如下:
- INSERT INTO `tb_topic`(`Title`) VALUES(?Title0)
- INSERT INTO `tb_topic`(`Title`) VALUES('Title_1')
在 new FreeSqlBuilder().UseNoneParameter(true) 全局设置
在 单次 ISelect,IInsert,IDelete,IUpdate 上使用 NoneParameter() 设置单次生效
3,Dto 映射查询
用过 ProjectTo 功能吗? 没用过当忽略此行...
有些朋友可能是先 ToList().Mapper<T>(), 这样会先查询了所有字段.
Dto 映射查询支持单表 / 多表, 这个功能可以决定只查询部分字段(不是, 不是, 不是先查询所有字段再到内存映射).
规则: 查找属性名, 会循环内部对象 _tables(多表会增长), 以 主表优先查, 直到查到相同的字段.
如: A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被映射. 也可以指定 id = C.id 映射.
- fsql.Select<Song>().ToList(a => new DTO {
- xxx = a.ext
- })
- // 情况 1: 附加所有映射, 再额外映射 ext, 返回 List<DTO>
- fsql.Select<Song>().ToList(a => new Song {
- id = a.id
- })
- // 情况 2: 只查询 id, 返回 List<Song>
- fsql.Select<Song>().ToList(a => new {
- id = a.id
- })
- // 情况 3: 只查询 id, 返回 List < 匿名对象>
- fsql.Select<Song>().ToList(a => new DTO(a.id))
- // 情况 4: 只查询 id, 返回 List<DTO>
- fsql.Select<Song>().ToList(a => new DTO(a.id) {
- xxx = a.ext
- })
- // 情况 5: 查询 id, ext, 返回 List<DTO>
- fsql.Select<Song>().ToList(a => new Song(a.id))
- // 情况 6: 查询 id, 返回 List<Song>
- fsql.Select<Song>().ToList(a => new Song(a.id) {
- xxx = a.ext
- })
- // 情况 7: 查询 id, ext, 返回 List<Song>
- 4,WhereCascade
FreeSql 擅长多表查询, 遇到像 isdeleted 每个表都给条件的时候, 挺麻烦. WhereCascade 使用后生成 sql 时, 所有表都附上这个条件.
如:
- fsql.Select<t1>()
- .LeftJoin<t2>(...)
- .WhereCascade(x => x.IsDeleted == false)
- .ToList();
得到的 SQL:
- SELECT ...
- FROM t1
- LEFT JOIN t2 on ... AND (t2.IsDeleted = 0)
- WHERE t1.IsDeleted = 0
其中的实体可附加表达式时才生效, 支持子表查询. 单次查询使用的表数目越多收益越大.
5, 审计 CURD
如果因为某个 sql 骚操作耗时很高, 没有一个相关的审计功能, 排查起来可以说无从下手.
FreeSql 支持简单的类似功能:
- fsql.Aop.CurdAfter = (s, e) => {
- if (e.ElapsedMilliseconds> 200) {
- // 记录日志
- // 发送短信给负责人
- }
- };
只需要一个事件, 就可以对全局起到作用.
还有一个 CurdBefore 在执行 sql 之前触发, 常用于记录日志或开发调试.
6, 审计属性值
实现插入 / 更新时统一处理某些值, 比如某属性的雪花算法值, 创建时间值, 甚至是业务值.
- fsql.Aop.AuditValue += (s, e) => {
- if (e.Column.CsType == typeof(long)
- && e.Property.GetCustomAttribute<SnowflakeAttribute>(false) != null
- && e.Value?.ToString() == 0)
- e.Value = new Snowflake().GetId();
- };
- class Order {
- [Snowflake]
- public long Id { get; set; }
- //...
- }
当属性的类型是 long, 并且标记了 [Snowflake], 并且当前值是 0, 那么在插入 / 更新时它的值将设置为雪花 id 值.
说明: SnowflakeAttribute 是使用者您来定义, new Snowflake().GetId() 也是由使用者您来实现
如果命名规范, 可以在 aop 里判断, if (e.Property.Name == "createtime") e.Value = DateTime.Now;
还有.. 还有很多骚操作.. 不便在此展开...
十, 展望 2020
2019 年支持了主流的数据库:
SqlServer 2000-2019, 支持 row_number/offset fetch next 分页自动版本选择适配, 以及其他语法的差异适配, 提供 ado.NET 与 odbc 两种实现方式;
PostgreSQL 9.4-12, 完成了版本间部分差异适配, 提供 ado.NET 与 odbc 两种实现方式;
MySQL 5.5,Mariadb, 提供 Oracle 官方驱动, 与 MySqlConnector 社区驱动, 还有 odbc 实现方式;
Oracle 11+, 提供 ado.NET 与 odbc 两种实现方式;
SQLite, 兼容了 .net core / .net framework / xamarin 平台适配, 支持 CodeFirst 开发模式, 一个字爽!!!
MsAccess 2003-2007, 提供 oledb 实现方式, 支持 CodeFirst 开发模式;
达梦, 提供 odbc 的实现方式, 并且支持 DbFirst 和 CodeFirst 两种开发模式;
2020 年支持国产是重点, 重心, 重要的工作内容, 南大通用将是下一个目标, 并且已经在进行中了.
开源地址: https://github.com/2881099/FreeSql
写到最后面, 感谢这一年来与 FreeSql 一直陪伴的兄弟朋友们.
来源: https://www.cnblogs.com/kellynic/p/12098419.html