FreeSql 实战:从 CRUD 到并发控制,看清 .NET ORM 选型

发布时间:2026/10/20 21:29:52
FreeSql 实战:从 CRUD 到并发控制,看清 .NET ORM 选型 做 .NET 后端这几年几乎每个新项目的第一步都在纠结 ORM 选型。最早跟风用 EF Core功能确实全可项目一复杂就觉得像背着一整套家具在跑步——模型配置、状态跟踪、延迟加载光理解这些概念就要花掉不少时间性能出问题还得一层层剥开去看。后来换 DapperSQL 全手写灵活性是满足了可实体多了以后Insert、Update、分页、关联查询这些重复代码写得我想骂人。直到朋友推荐我试了 FreeSql——也就是很多人打成 FreeSQL 的那款 .NET 生态开源轻量 ORM 框架——我才算找到一个平衡点。它确实轻一个IFreeSql实例、一个实体类增删改查就能跑起来但功能一点没缩水CodeFirst 自动建表、表达式查询、导航属性、仓储模式、多数据库支持全部内置。这篇文章就按我实际走过的路线从入门到实战把 FreeSql 的用法和踩过的坑整理出来适合刚开始接触 ORM 的新人也适合正在 EF Core 和轻量框架之间纠结的朋友。1. FreeSql 的定位不是又一个封装先想清楚它为谁解决什么问题1.1 轻量体现在结构而不是阉割功能拿到 FreeSql 之后我第一反应是看看它到底“包”了什么。它不像 EF Core 那样要求你先建一个 DbContext、配一堆 Fluent API它甚至不强制你用什么固定模式。围绕一个IFreeSql接口把Insert、Update、Delete、Select四大块能力组织起来看起来是轻型骨架内里却塞满常用功能。最典型的就是多数据库支持。以前用 EF Core 切库很多项目要换 Provider、改模型配置、调整迁移脚本折腾半天。FreeSql 在同一个模型上改一行DataType和连接字符串就能换库跑。我有个内部工具从 MySql 切到 SqlServer业务代码几乎没动。这背后是它自己实现了一套 SQL 方言翻译层把不同数据库在分页、自增、类型映射上的差异尽量挡在框架内部。打个比方EF Core 是装修公司什么都替你干了但费用和等待时间也跟着上来Dapper 是一把好用的美工刀灵活但所有细节都要自己动手FreeSql 更像一个顺手的工具箱常用工具都给你备好拿起来就能干活还不占地方。1.2 和 EF Core、Dapper 的选型对比我自己的判断标准很多人问我有 EF Core 和 Dapper 了为什么还要用 FreeSql。我的看法是它们并不是单纯的好与坏而是各自服务的场景不同。我习惯先列一张对比表再下结论维度FreeSqlEF CoreDapper是否必须手写 SQL不需要但也支持原生 SQL不需要主要手写自动建表CodeFirst 直接同步Migration 管理不支持导航属性支持支持不支持表达式树查询强强弱性能接近 Dapper有差距最快学习成本中低中高低多数据库支持很全含国产数据库主流数据库主流数据库中文资料与社区丰富一般一般这个表基本就是我选型时的真实判断。如果你的项目追求极致查询性能又希望 SQL 完全可控Dapper 依然是对的如果团队有严格的数据库建模规范、需要版本化迁移文件EF Core 的价值不可替代。FreeSql 的生态位是中等复杂度的业务系统 不想被繁琐配置拖累的开发团队尤其是国内很多中小项目库表多、迭代快FreeSql 的入手成本和学习曲线都更友好。另外它还有很多开箱即用能力比如逻辑删除过滤器、审计日志、动态过滤、按时间分表这些都是框架内置的不需要自己写 AOP 或者重复模板代码。这些特性在我以前用 Dapper 的项目里全部要靠手写维护成本不低。2. 从零搭建安装、初始化与依赖注入的关键细节2.1 主包和 Provider 包怎么装用 .NET CLI 是最直接的方式dotnet add package FreeSql dotnet add package FreeSql.Provider.MySql第一行装的是核心包第二行是数据库 Provider。具体装哪个 Provider 取决于项目连什么库。我这边常用的包有这些FreeSql.Provider.SqlServerFreeSql.Provider.MySqlFreeSql.Provider.PostgreSQLFreeSql.Provider.SqliteFreeSql.Provider.Oracle这里提醒一个容易踩的坑主包和 Provider 包的版本号要尽量保持一致。我经历过主包升级、Provider 没跟着升的情况编译能通过但运行时驱动行为不一致排查起来非常费时间。建议用一个确定的版本号别每次装都默认最新版。2.2 FreeSqlBuilder 与全局单例初始化 FreeSql 通常用一个静态单例直接在应用启动时就建好using FreeSql; public static class Db { public static IFreeSql Instance { get; } new FreeSqlBuilder() .UseConnectionString(DataType.MySql, Serverlocalhost;Port3306;Databasemy_db;Uidroot;Pwd123456;Charsetutf8mb4;) .UseAutoSyncStructure(true) .UseMonitorCommand(cmd Console.WriteLine(cmd.CommandText)) .Build(); }这段配置里的每个成员都有自己的讲究。UseConnectionString里的DataType必须和 Provider 包对应它是方言引擎的开关UseAutoSyncStructure(true)表示开发期实体结构变化后自动同步表结构相当于把建表脚本交给框架维护UseMonitorCommand会把每次执行的 SQL 打到控制台是我调试 SQL 依赖度最高的功能。重要不要每次请求都 new FreeSqlBuilder() 创建新实例。IFreeSql内部维护连接池和表达式编译缓存官方推荐用法就是全局单例。我之前在一个早期项目里犯过这个错请求量一上来连接池直接被打爆后来才意识到这个框架本身是线程安全的建一个实例到处用反而是性能最优解。2.3 注册到依赖注入拿一个可使用的入口ASP.NET Core 里注册非常简单builder.Services.AddSingleton(Db.Instance);之后任何服务通过构造函数就能拿到IFreeSql。如果项目想用仓储风格还可以注册泛型仓储builder.Services.AddScoped(typeof(IBaseRepository), typeof(BaseRepository));仓储模式不是必须的但是项目一旦规模上来它能省很多重复代码我放到第 5 章详细说。第一次跑通之后你一定会想试试增删改查不过在那之前要先让 FreeSql 认识你的实体和表结构这就是第 3 章的实体映射。3. 实体映射和 CodeFirst 建表从写类开始表是长出来的3.1 约定与特性Table、Column、NavigateFreeSql 有自己的默认约定实体类名对应表名属性名对应列名主键默认是Id或者实体名 Id。如果只是做 Demo什么都不配也行。但真实项目里通常要建一套自己的命名规范我一般会主动使用特性让表和列名显式可控[Table(Name tb_user)] public class User { [Column(IsPrimary true, IsIdentity true)] public int Id { get; set; } [Column(Name nick_name, DbType varchar(50))] public string NickName { get; set; } public int Age { get; set; } [Column(IsIgnore true)] public string Tips { get; set; } }IsPrimary和IsIdentity代表主键和自增DbType用来干预生成列的类型IsIgnore表示这个属性不参与表结构映射。还有IsNullable、控制字段是否允许为空等处理 bool、枚举字段的类型映射时非常有用。举个例子C# 的bool默认可能映射成tinyint(1)如果你希望映射成bit直接在列上指定DbType就能解决。导航属性的关联也靠特性。比如一个用户有多条订单[Table(Name tb_order)] public class Order { [Column(IsPrimary true, IsIdentity true)] public int Id { get; set; } public int UserId { get; set; } [Navigate(nameof(User.Id))] public User User { get; set; } }Navigate告诉 FreeSql 当前外键对应目标实体的哪一列。它既能描述一对多也能描述多对多后续查询直接当普通属性用就行。3.2 自动同步表结构的正确姿势UseAutoSyncStructure(true)开启后应用启动时框架会比较实体和现有表发现缺表会自动建表发现缺字段会自动加列。开发环境里这个功能太省心了我再也不用写一堆CREATE TABLE脚本改完实体重启就能看到新列。但这个功能有个使用边界生产环境尽量不要开。原因很简单如果多个服务实例同时启动可能发生多个进程同时改表结构轻则锁表重则直接报错。而且生产表结构变更必须走 review 和迁移流程让框架默默改表风险太大。我的习惯是开发环境开生产环境关上线前用测试环境把建表脚本重新生成一遍交给 DBA 审核执行。如果你更习惯手工控制也可以不开自动同步在应用启动时手动指定实体类型fsql.CodeFirst.SyncStructure(typeof(User), typeof(Order));这样更可控。模型就绪之后就可以进入最核心的 CRUD 实战了。4. CRUD 实战FreeSql 单表操作的花式用法与习惯4.1 Insert插入数据与拿自增主键写数据最基础的就是插入。FreeSql 的 API 设计得很连续读起来像在造句var user new User { NickName 阿飞, Age 28 }; var id fsql.InsertUser() .AppendData(user) .ExecuteIdentity();AppendData传入实体链式接ExecuteIdentity()会返回自增主键。这个场景很适合插入后马上拿 ID 继续写关联数据。如果不需要主键只想知道影响行数用ExecuteAffrows()。批量插入也一样自然var rows fsql.InsertUser() .AppendData(userList) // userList 是 ListUser .ExecuteAffrows();这里最深刻的体会是别写循环逐条插入。每调一次 Insert 都是一次数据库往返循环 1000 条就是 1000 次往返批量一条 SQL 就能搞定性能差距可能是一个数量级。我见过有人因为图省事在循环里调ExecuteAffrows()线上巡检时一条插入接口耗时几百毫秒改成批量后直接降到几十毫秒。4.2 Update整体更新还是指定字段更新更新有两种常见姿势。第一种是整实体更新适合表单回写场景var user fsql.SelectUser().Where(a a.Id 1).ToOne(); user.NickName 新名字; var rows fsql.UpdateUser() .SetSource(user) .ExecuteAffrows();SetSource(user)把实体所有字段更新一遍主键除外。但有个细节要注意如果实体字段很多而实际上只改动了一两个字段SetSource依然会更新所有列这会带来不必要的网络开销和潜在的并发覆盖。我更推荐第二种方式——只更新必要的列var rows fsql.UpdateUser() .Set(x x.NickName, 新昵称) .Set(x x.Age, x x.Age 1) .Where(x x.Id 10) .ExecuteAffrows();第二个Set的新值可以直接依赖原值它会翻译成 SQL 里的age age 1不需要先查出来再在 C# 里自增。这种表达既清晰又安全还能少一次查询往返。4.3 Delete条件删除与逻辑删除的提醒删除也很直接fsql.DeleteUser().Where(x x.Id 1).ExecuteAffrows(); fsql.DeleteUser().Where(x x.Age 18).ExecuteAffrows();条件由表达式直译过去不需要写DELETE FROM ...。要清空全表写Where(a true)就行但生产环境千万慎用。另外建议大家优先考虑逻辑删除而不是物理删除。FreeSql 内部有全局过滤器可以在表上配置逻辑删除字段之后SELECT会自动追加未删除条件用户侧的查询代码完全不用改数据却不会真的丢失。4.4 Select查询、投影与分页单表查询var list fsql.SelectUser() .Where(x x.Age 18 x.NickName.Contains(张)) .OrderBy(x x.Id) .Skip(0).Take(20) .ToList();Contains会翻译成LIKE %张%OrderBy、Skip、Take都有语法跟 LINQ 很像写过一点点 LINQ 的人基本零成本上手。分页我更推荐封装好的ToPageListvar list fsql.SelectUser() .Where(x x.Age 18) .OrderByDescending(x x.Id) .ToPageList(1, 20, out var total);这里注意页码从 1 开始计数out total返回总条数。它的底层是两次查询一次COUNT(*)、一次数据页比自己拼 SQL 干净得多也不容易漏掉总条数统计。查询时还有一个值得养成的习惯按需投影。如果查询只需要其中两个列就不要图省事SELECT *var dtos fsql.SelectUser() .Where(x x.Age 0) .Select(x new UserDto { Id x.Id, Name x.NickName }) .ToList();这样能减少无谓的大字段传输SQL 性能和网络带宽都会更好。5. 进阶用法表达式树、导航属性、仓储模式与工作单元5.1 表达式树查询和子查询的实际威力FreeSql 和 EF Core 最接近的地方就是支持把 C# 的 lambda 表达式翻译成 SQL。你写的是强类型 C#数据库拿到的却是带占位参数的 SQLfsql.SelectUser() .Where(a a.NickName.Contains(张) || a.Age 18) .ToList();我最初不太敢用复杂表达式总担心会产生奇怪的 SQL。后来开着UseMonitorCommand的 SQL 日志亲眼看到它把Contains、Any、Select都转成对应的方言和参数心里才踏实。FreeSql 对表达式支持的范围挺广日常遇到的In、Not In、字符串拼接、日期比较都能翻译。最常用的是子查询写法fsql.SelectUser() .Where(a fsql.SelectOrder() .Where(o o.UserId a.Id) .Any(o o.Amount 100)) .ToList();这段会被翻译成EXISTS (SELECT ... FROM tb_order o WHERE o.user_id a.id AND o.amount 100)类型的子查询业务含义就是找出下过超百元订单的用户。注意嵌套写法会自动关联外层实体的a.Id和外键比自己拼接子查询安全得多。5.2 导航属性与 IncludeMany关联查询不再手写导航属性是 FreeSql 提效的重点。在实体上定义好外键和Navigate之后查询时可以一次性把关联数据带出来var users fsql.SelectUser() .IncludeMany(a a.Orders) .ToList();访问users[i].Orders时集合是已经加载好的不需要再单独写一次fsql.SelectOrder().Where(o o.UserId ...)。实际执行时FreeSql 会先查主表再根据主表筛选出来的主键列表发起关联查询从数据库往返次数上看依然很省。多对多关系也支持比如学生和课程的经典场景需要中间表。实体上配置[Navigate(ManyToMany typeof(StudentCourse))]这类信息IncludeMany会自动把中间表的数据汇聚成右侧的列表。这个我在项目里用得相对少用到时翻官方文档查一下示例即可。不过要提醒一个容易误会的点FreeSql 默认不会像 EF Core 那样自动懒加载导航属性。你需要手动Include或IncludeMany这是我比较喜欢的设计因为查询变慢时你至少知道是自己明确要求的加载逻辑在起作用而不是框架偷偷帮你查了一大堆关联数据。5.3 仓储模式与工作单元业务层如何组织数据访问FreeSql 的仓储模式是可选能力但和依赖注入放一起很舒服var repo fsql.GetRepositoryUser(); var u new User { NickName 仓库用户, Age 30 }; repo.Insert(u); u.Age 31; repo.Update(u);仓储对象会跟踪实体状态Update(u)会按主键更新整个实体而且如果实体上有Version字段它还会自动带上乐观锁检查。仓储很适合在业务服务里把同一个实体的操作聚合起来代码读起来清晰也避免fsql.InsertUser、fsql.UpdateUser散落得到处都是。事务方面我最推荐工作单元using var uow fsql.CreateUnitOfWork(); var userRepo uow.GetRepositoryUser(); var orderRepo uow.GetRepositoryOrder(); userRepo.Insert(new User { NickName 事务用户, Age 25 }); orderRepo.Insert(new Order { UserId 新用户Id, Amount 199 }); uow.Commit();CreateUnitOfWork()会开启一个事务范围内的会话内部所有仓储操作都在同一个连接上执行uow.Commit()统一提交如果中途异常没有调用 CommitDispose时自动回滚。我在接口里同时写主表和明细表时基本都用这一套比手动管理Ado.Transaction的代码干净得多。6. 实战问题记录事务边界、并发控制与多数据库切换的隐性坑6.1 事务谁要一起成功谁必须独立写事务的第一个原则是范围能小则小。工作单元虽然方便但事务连接会被独占占用时间越短越好。我在一个数据导入接口里吃过亏一开始把 Excel 解析加批量 Insert 全包在一个工作单元里结果数据量一大事务跑了好几秒数据库连接被长期占用服务吞吐明显掉下来。后来把 Excel 解析移到事务外面只有真正写库的批量操作才进事务问题才解决。第二个原则是明确哪些操作必须原子。比如创建订单时同时插主表和明细表这两步必须同生共死。但写订单 发通知消息就不能放同一个事务消息发送失败不应该回滚订单。事务本质是用资源换一致性别什么都往里塞。6.2 乐观锁与并发冲突处理高并发更新场景我基本只用乐观锁。FreeSql 支持在实体上标记Version字段实现自动版本控制但我更常用手动控制的方式逻辑更透明var old fsql.SelectUser().Where(a a.Id 1).ToOne(); if (old null) return; var rows fsql.UpdateUser() .Set(x x.NickName, 新名字) .Set(x x.Version, old.Version 1) .Where(x x.Id old.Id x.Version old.Version) .ExecuteAffrows(); if (rows 0) { // 版本冲突说明别人先改了需要重新读取或提示用户 }这个写法的核心是版本相等才允许更新被翻译进 WHERE 条件ExecuteAffrows()返回 0 就代表冲突。比起先查再无条件更新它能真正避免丢失更新。至于悲观锁FOR UPDATE在某些数据库里支持得很好但 FreeSql 跨数据库时尽量不要依赖它因为不同厂商的锁语法和行为细节差异很大很容易在切换库时踩雷。6.3 多数据库切换时的隐性坑FreeSql 的卖点是换库不换代码但我的真实体验是大部分 CRUD 确实不用改可边界条件很多。比如自增主键MySql 是auto_incrementSqlServer 是IDENTITY(1,1)PostgreSQL 是SERIAL或GENERATED AS IDENTITY。你用ExecuteIdentity()取主键的代码不用变但数据库里如果用了特殊 DDL 或默认值就要小心兼容性。还有一个更常见的坑fsql.Ado.ExecuteSql直接写带数据库特有语法的 SQL。我在 MySql 里跑得好好的分页写法换到 SqlServer 直接语法错误。后来我给自己定了一条规矩跨库业务代码只用 FreeSql 统一 API真要手写原生 SQL就按 DataType 做分支var isMySql fsql.Ado.DataType DataType.MySql; var sql isMySql ? 原生SQL-A : 原生SQL-B; fsql.Ado.ExecuteSql(sql);这样核心业务代码不散落数据库方言切换库时只需要维护少数几个分支点。最后再分享一点自己的实战体会框架再好也只是把数据库访问的常见模式封装了一层底层的事务、索引、SQL 优化这些基本功还是得过关。我实际用 FreeSql 这一年多最大的受益是开发效率明显提升写数据访问层的时间至少缩短一半省下来的时间去调索引、看执行计划反而对数据库的理解更深了。建议从第一个 Demo 跑顺之后多做实验改条件、加字段、打开 SQL 日志、看执行计划。等你能习惯性地从日志里读出这条查询走了全表扫描、那条更新带了版本条件FreeSql 就不再只是一个工具而成为了你设计数据访问思路的一部分。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询