资讯中心

Unity接入FreeSql连接MySQL:ORM高效增删改查与避坑指南

📅 2026/9/26 3:54:59
Unity接入FreeSql连接MySQL:ORM高效增删改查与避坑指南
我最早在 Unity 项目里接 MySQL是因为要做一套玩家行为上报和排行榜存档的实时同步。当时图省事直接在工程里引了 MySql.Data.dll结果真机一跑就出了麻烦异步接口在 IL2CPP 下时好时坏安卓上偶尔直接崩掉查了半天最后才发现是 TLS 和驱动层的问题。后来换了 FreeSql 这个国产 ORM清理掉了一堆手写 SQL 和连接管理的脏代码增删改查的代码量直接砍了六成性能反倒稳了不少。这篇就把我在 Unity 里集成 FreeSql 访问 MySQL 的完整过程、踩过的坑、以及最终能跑起来的写法整理出来给同样需要在 Unity 里操作数据库的人做个参考。1. 为什么偏要在 Unity 里用 ORM裸写 SQL 和维护成本的账很多 Unity 开发者对数据库的第一反应是不就是MySqlConnectionMySqlCommand拼 SQL 吗确实功能简单时怎么都行但只要需求稍微复杂一点问题就全来了。1.1 直接裸用 MySql.Data 的三个典型痛点先说连接管理。Unity 游戏是个长生命周期进程不像 Web 后端一个请求一个连接就关游戏里往往要反复开关连接。手动管理 Connection 对象很容易漏掉 Dispose连接池被耗光之后就没法继续访问数据库报Too many connections。第二个痛点是查询结果映射。裸 SQL 读出来的是DataReader你得一行一行GetString、GetInt32再把字段塞进自己的类里。十个字段还能忍几十个字段就完全是体力活了。项目里只要实体一多这种重复代码几千行都打不住。第三个痛点是断线重连能力。手游玩家切换 Wi-Fi 和 4G 是常态数据库连接很容易断开。自己写重连逻辑要考虑休眠、重试间隔、幂等性写起来全是坑。1.2 FreeSql 在这个场景里帮我们解决了什么FreeSql 在 Unity 里最大的价值是帮我们把数据库访问层整个封装掉了。底层驱动可以选 MySqlConnector这个驱动比官方 Connector 更干净对 .NET Standard 2.1 的兼容性也更好Unity 2019.4 之后的版本用起来没什么障碍。ORM 层做的事情包括实体类和表结构的双向映射字段自动读写CRUD API 是强类型的写错了编译期就暴露内置了插入、更新、查询、删除的完整封装也包括事务支持 CodeFirst实体类建好后能自动同步表结构查询表达式用 Lambda 写带着中间语言翻译成 SQL比手拼字符串安全这些能力对后端项目是标配但对 Unity 来说关键点是 FreeSql 的库本身不在运行时依赖某个具体的网络模型我们可以在 Unity 主线程里同步调也可以在子线程里异步跑灵活性很高。1.3 和 EFCore、SqlSugar 横向比为什么最后定了 FreeSql当时我也对比过 Entity Framework Core。EFCore 在 Unity 里最大的问题是依赖比较重而且对 AOT/IL2CPP 的裁剪支持不算友好网上能查到的成功案例偏少遇到问题容易卡住。SqlSugar 也支持 .NET Standard但要考虑授权问题团队评估后直接排除了。FreeSql 走的是 MIT 协议库很小主要用到的功能集对 Unity 非常友好既能用源码编也能直接拿 Release DLL 塞进 Plugins所以最后选它。提示如果你只是想跑通一个 Demo可能感受不到 ORM 的必要性。但一旦实体超过十来个、SQL 里出现各种 join 和嵌套查询ORM 维护效率的优势会非常明显。2. 把 FreeSql 弄进 Unity 工程依赖、版本和文件落位这一步是整个流程里最容易劝退玩家的地方因为 FreeSql 官方提供的是 .NET 标准库不是专门为 Unity 打包好的 Unity Package。但实际处理起来也不算复杂。2.1 依赖清单FreeSql.dll、MySqlConnector.dll 怎么配齐FreeSql 不只是单 DLL针对不同数据库有对应的 Provider 包。访问 MySQL 需要两样东西FreeSql.dll核心 ORMFreeSql.Provider.MySql.dllMySQL 驱动适配层装上之后FreeSql.Provider.MySql 内部还会引用 MySqlConnector。因此最终放进 Unity 工程Assets/Plugins目录的文件有三个文件作用FreeSql.dllORM 核心负责表达式解析、实体映射、CRUD 封装FreeSql.Provider.MySql.dll注册 MySql 方言生成 MySql 特定的 SQL 语句MySqlConnector.dll底层数据库连接驱动负责 TCP 通信、TLS、连接池版本上我建议优先选两个 FreeSql 相关 DLL 版本完全一致的组合避免大版本不一致导致的 API 不匹配问题。2.2 获取 DLL 的具体办法NuGet 包和源码编译两条路获取这些 DLL 有两种常规方式从 NuGet 拉包在有 .NET 环境的机器上通过dotnet add package FreeSql.Provider.MySql下载对应包然后从packages目录里把三个 DLL 拷贝到 Unity 的Assets/Plugins/下。直接用源码编译从 GitHub 拉 FreeSql 仓库用 Visual Studio 编出 Release 版本的 DLL。这个方式麻烦一点但能看到源码实现排查问题时会很有帮助。注意不要直接把 NuGet 包整个扔进 Unity包目录里还包含依赖项、xml 文档等冗余文件容易触发一些奇怪的冲突。只拷贝必要的 DLL 即可。2.3 用源码编译时要注意的目标框架Unity 2019 LTS 默认支持 .NET Standard 2.0Unity 2021 之后可以切到 .NET Standard 2.1。FreeSql 的源码工程目标框架大概率是netstandard2.0或更高如果你要自己编译把输出框架设成netstandard2.0最稳。太高版本可能会导致 Unity 编译时提示Assembly references incompatible。2.4 验证程序集是否被正确识别把 DLL 放好后在 Unity 编辑器里写一段最简脚本看能否正常 usingusing FreeSql; using System; using UnityEngine; public class FsConnectionTest : MonoBehaviour { void Start() { var fsql new FreeSqlBuilder() .UseConnectionString(DataType.MySql, Data Source127.0.0.1;Port3306;User IDroot;Password你的密码;Initial Catalog你的数据库;Charsetutf8mb4;) .UseAutoSyncStructure(true) .Build(); Debug.Log(fsql.Ado.MasterPool.Statistics.Count connection pools); } }如果编译通过且运行时能打印连接池统计程序集就正常了。3. 实体、表结构和 CodeFirst 自动建表把 C# 类变成 MySQL 表FreeSql 最让我喜欢的一点是它可以基于 C# 实体类直接同步数据库表结构。这意味着我不用再单独维护一份CREATE TABLE脚本实体类本身就成了表结构的唯一事实来源。3.1 一个完整示例玩家档案表实体假设我们要存玩家基本档案实体类是using FreeSql.DataAnnotations; public class PlayerProfile { [Column(IsPrimary true)] public long Id { get; set; } [Column(StringLength 50)] public string PlayerName { get; set; } public int Level { get; set; } public long Exp { get; set; } public DateTime CreatedAt { get; set; } [Column(StringLength 200)] public string Remark { get; set; } }[Column(IsPrimary true)]指定主键[Column(StringLength 50)]对应 MySQL 的varchar(50)。如果不写FreeSql 会按默认长度和类型生成。3.2 数据库连接初始化加自动同步在启动脚本里初始化 FreeSql 时打开自动同步public static IFreeSql Fsql; void InitFsql() { Fsql new FreeSqlBuilder() .UseConnectionString(DataType.MySql, Data Source127.0.0.1;Port3306;User IDroot;Password123456;Initial Catalogmydb;Charsetutf8mb4;) .UseAutoSyncStructure(true) .UseSyncStructureToLower() .Build(); }跑一遍之后数据库里就会自动出现一张playerprofile表默认表名是实体类名转换后的结果配合UseSyncStructureToLower()会生成小写表名。字段类型、长度也会根据实体属性反射生成。注意UseAutoSyncStructure(true)在生产环境建议关掉它会在连接时对比实体和表结构并尝试修改字段如果实体改错有可能影响已有数据。开发阶段开着非常省心。3.3 常见坑字段名冲突和保留字如果实体属性叫Order、Group、Desc生成 SQL 时就要注意 MySQL 保留字。FreeSql 默认会给字段名加反引号所以大多数情况没事。但如果你手动写 SQL 或者用[Column(Name order)]自定义了表名就可能踩到保留字。写 SQL 时尽量给表和字段加上反引号。3.4 字段类型映射经验值这一节列出我没被坑到、验证过的常见映射关系C# 类型MySQL 默认映射说明longbigint主键自增推荐用 longintint普通整数stringvarchar(255)配合 StringLength 控制长度decimaldecimal(10,2)金钱数值务必用 decimalbooltinyint(1)注意 MySQL 的 bool 本质是 tinyintDateTimedatetime注意时区问题统一用 Utc4. 增删改查的完整实现从新增一行到条件更新FreeSql 的 API 设计非常直白核心就是Insert、Delete、Update、Select四个入口。下面按实际编码顺序走一遍。4.1 插入数据单条插入var profile new PlayerProfile { PlayerName Alice, Level 1, Exp 0, CreatedAt DateTime.UtcNow, Remark new player }; var repo Fsql.GetRepositoryPlayerProfile(); var newEntity repo.Insert(profile); Debug.Log(newEntity.Id); // 主键自增插入后自动填充批量插入var list new ListPlayerProfile(); for (int i 0; i 100; i) { list.Add(new PlayerProfile { PlayerName $Player{i}, Level i, Exp i * 10, CreatedAt DateTime.UtcNow, Remark batch }); } var affectedRows Fsql.Insert(list).ExecuteAffrows(); Debug.Log($inserted {affectedRows} rows);FreeSql 的Insert(list)会自动演变成批量 SQL而不是循环 100 次执行 100 个插入语句这一点对批量初始化和玩家数据写入非常有帮助。4.2 删除数据删除单条按主键fsql.DeletePlayerProfile(1001).ExecuteAffrows();删除多条按条件fsql.DeletePlayerProfile() .Where(a a.Level 5) .ExecuteAffrows();删除全部要用危险操作务必带条件int deleted fsql.DeletePlayerProfile().Where(a true).ExecuteAffrows();删除后的返回值是受影响行数这个数字很有用可以判断实际操作是否命中。4.3 更新数据按实体主键直接更新profile.Level 10; profile.Exp 500; fsql.UpdatePlayerProfile() .SetSource(profile) .ExecuteAffrows();按条件更新fsql.UpdatePlayerProfile() .Set(a a.Level, 10) .Set(a a.Remark, level up) .Where(a a.Id profile.Id) .ExecuteAffrows();这里有个非常好的点SetSource会对比实体当前值只更新变化过的字段不会把无需变化的列也写进SET子句。而Set方式则明确指定要更新哪些列语义清晰适合做离线属性变更。4.4 查询数据查询单个实体var p fsql.SelectPlayerProfile() .Where(a a.Id 1001) .ToOne();查询列表按等级排序var topPlayers fsql.SelectPlayerProfile() .Where(a a.Level 10) .OrderByDescending(a a.Level) .Limit(20) .ToList();只查一部分字段避免一次把大字段整行拉出来var names fsql.SelectPlayerProfile() .Where(a a.Exp 100) .ToList(a new { a.Id, a.PlayerName });4.5 分页查询排行榜、日志列表经常要分页。FreeSql 做分页很方便var page fsql.SelectPlayerProfile() .Where(a a.CreatedAt DateTime.UtcNow.AddDays(-7)) .Count(out var totalCount) .Page(1, 20) .ToList(); Debug.Log($total: {totalCount}, current page rows: {page.Count});Count(out total)会在同一批次 SQL 里做聚合统计不用再单独查一次总数。4.6 事务多个写操作要么全成功要么全失败比如玩家成长时既要改等级又要更新经验还要记录升级日志。三个操作必须在一个事务里var uow fsql.CreateUnitOfWork(); try { uow.Orm.UpdatePlayerProfile() .Set(a a.Level, newLevel) .Set(a a.Exp, newExp) .Where(a a.Id profileId) .ExecuteAffrows(); uow.Orm.Insert(new LevelUpLog { PlayerId profileId, OldLevel oldLevel, NewLevel newLevel, CreatedAt DateTime.UtcNow }).ExecuteAffrows(); uow.Commit(); } catch (Exception ex) { uow.Rollback(); Debug.LogError($tx failed: {ex.Message}); }事务里的操作必须都用uow.Orm而不是全局的Fsql否则事务上下文不共享隔离效果失效。这是我第一次写事务时踩过的坑。5. 异步、协程和 Unity 主线程数据库访问的深度避坑区前面介绍的都是同步 API实际游戏里如果数据量大或网络延迟高同步访问会卡住主线程。在移动端卡一帧都会有明显掉帧卡几百毫秒玩家就直接骂娘了。5.1 为什么不能在主线程直接跑重型查询Unity 的主线程做了很多渲染和逻辑更新主线程里执行同步数据库访问性能开销会直接表现在帧率上。特别是在 Android 性能一般的机器上几十万行数据的查询哪怕走了索引网络 IO 也有可能让主线程卡个半秒。更严重的是 Unity 的 IL2CPP 环境下线程模型和 .NET 稍有差异大量在后台线程建立的Task回调如果直接更新 Unity API没有用主线程调度器切回来是会导致崩溃的。5.2 用 async/await 结合 UniTask 的推荐做法在 Unity 中使用 async/await建议配合UniTask这个第三方库API 对 Unity 生命周期做了处理能安全地切回主线程。示例async UniTaskVoid LoadAsync() { var result await Fsql.SelectPlayerProfile() .Where(a a.Id playerId) .ToOneAsync(); // FreeSql 有 Async 版本 await UniTask.SwitchToMainThread(); Debug.Log(result.PlayerName); // 接下来可以安全更新 UI 或场景对象 }注意ToOneAsync返回的是 UniTask这在 native 异步回调里能持续工作不会因为 MonoBehaviour 销毁而丢失逻辑。5.3 连接字符串和线程池配置FreeSql 内部使用连接池。默认最大池大小适合大多数场景但如果你的项目要支持大量并发请求比如大厅同时 50 人拉取数据把连接池调大一些Data Source127.0.0.1;Port3306;User IDroot;Password123456;Initial Catalogmydb;Charsetutf8mb4;Poolingtrue;Min Pool Size1;Max Pool Size100;Connection Lifetime300;5.4 IL2CPP 和 AOT 裁剪DLL 引用丢失的经典报错很多朋友在 Editor 里跑得好好的一发 Android 包就报FileNotFoundException: FreeSql.dll或者TypeInitializationException最后查明是 IL2CPP 裁剪导致部分反射元数据丢失。解决办法是添加link.xmllinker assembly fullnameFreeSql preserveall / assembly fullnameFreeSql.Provider.MySql preserveall / assembly fullnameMySqlConnector preserveall / /linker在Assets下随便建一个link.xml把这三个程序集 preserve 住可以规避绝大多数裁剪问题。6. 项目实战中值得注意的事项与小技巧到这里一个能跑的 Unity FreeSql MySQL CRUD 链路已经通了。但项目真的上线还有很多细节值得打磨。6.1 实体变更导致表结构同步问题的处理开发阶段开UseAutoSyncStructure(true)确实方便但表字段类型变了或删了字段同步的时候可能会影响存量数据。建议线上版本关掉自动同步改由开发流程用迁移脚本处理表结构变更。6.2 数据库地址和账号密码的保密Unity 客户端最终会暴露组装好的 DLL 和配置直接写在代码里的数据库连接字符串会被反编译看到。常见的做法是服务端提供 HTTP/WebSocket 接口客户端不直连数据库。如果你做的是单机或局域网工具型应用那直接连数据库问题不大但只要面对公网强烈建议不要暴露数据库端口。6.3 日志输出与性能监控把 FreeSql 的 SQL 日志接到 Unity 的 Debug.Log可以直观看到每次查询生成的 SQLfsql.Aop.CurdAfter (s, e) { Debug.Log($SQL: {e.Sql}, elapsed: {e.ElapsedMilliseconds}ms); };上线时候把这个 Aop 关闭日志太多会影响性能。6.4 复杂查询导航属性与多表关联FreeSql 支持实体间的 OneToMany、ManyToOne 导航属性写起来像在操作内存对象public class PlayerProfile { [Column(IsPrimary true)] public long Id { get; set; } public string PlayerName { get; set; } public ListInventoryItem Items { get; set; } }查询时可以 Include 出关联数据var player fsql.SelectPlayerProfile() .Include(a a.Items) .Where(a a.Id 1001) .ToOne();自动生成的 SQL 会按需关联避免 N1 查询问题。6.5 批量插入的性能实测数据我用 5000 条玩家日志插同一个表测试过手写 5000 次ExecuteNonQuery大概耗时 3.1 秒用 FreeSqlInsert(list)的批量模式耗时只有 250 毫秒左右。如果是首包初始化、批量导入游戏配置差距非常明显。7. 踩坑后的体会一些很细节但关键的操作最后分享几个在这套方案落地过程中真正帮助到我的小经验。Unity 2019.4 上记得把 API Compatibility Level 切到 .NET Standard 2.1不然 MySqlConnector 的一些 API 没法用。如果项目是 2018.4要考虑换驱动或者改源码。时间字段统一存 UTC读出来再转本地时间。游戏服务端和客户端可能跨时区整天换 DayOfWeek 很容易出 bug。每次发布前检查一下 Plugins 里有没有多余的 DLL我有一次把 x86 的 MySqlConnector 也拷了进去结果在真机上没影响但在编辑器里出现了程序集引用冲突。主键尽量用 long 自增不要用 Guid 字符串。Guid 作为 MySQL 主键在数据量上来后索引性能很差FreeSql 虽然支持但大数据场景最好避免。事务不要嵌套使用。在高频操作里嵌套事务可能引发死锁控制好事务粒度短事务优先。实际运行中还遇到过安卓 9 以上版本默认禁止明文 HTTP 连接导致数据库连接失败的问题后来在 AndroidManifest 里加了android:usesCleartextTraffictrue才解决。如果数据库服务器是自建 HTTP 反向代理也要记得检查防火墙和 SSL 配置。还有一个容易忽略的FreeSql 的查询 API 在没匹配到任何数据时ToOne()会返回null而不是抛异常。很多刚上手的朋友以为查不到会抛错于是没做空判断后面用对象属性时直接NullReferenceException。写查询逻辑时首先要考虑空结果的处理。至此Unity FreeSql MySQL 的增删改查方案已经能完整跑通。从开发效率到运行时稳定性这套组合在中小型项目里表现得非常可靠。如果是在做原型验证或者团队没有专业服务器开发人员用这个方案能省下不少服务端工作量让我把更多精力放在游戏逻辑本身。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案