Unity直连MySQL:用FreeSql实现高效CRUD与避坑指南

发布时间:2026/9/18 2:56:10
Unity直连MySQL:用FreeSql实现高效CRUD与避坑指南 在 Unity 项目里直接连 MySQL听起来有点像“不走寻常路”但碰到内部工具、离线演示或者中小型本地化应用时这反而是最省事的方案。前几天我重构了一个热身项目把原来的手写 ADO.NET 全部换成了 FreeSql增删改查的代码量直接缩水一半可读性和可维护性也上来了。这篇文章不打算讲高深理论就是把我踩过的坑、最终可落地的一套操作流程完整记录下来。适合这几类人看Unity 全栈开发者、需要在编辑器里做数据工具的 TA/程序以及那些想让客户端直接读写数据库、又不想写一堆重复 SQL 的同学。1. 整体设计与思路拆解1.1 什么场景下需要在 Unity 里直连 MySQL可能有人会说“Unity 里访问数据库为什么不写在后端客户端直连数据库太危险了。”这个观点我在大部分联机游戏里完全同意。但真实项目和工具类开发里常有几种场景绕不开。第一类是纯本机工具。比如公司内部的资源检查工具、数据配置工具Unity 界面只是壳真正要读写的是 MySQL 里的元数据。这时候再起一个 Web API 有点杀鸡用牛刀直连数据库反而最直接。第二类是局域网演示项目比如展厅里的互动大屏、沙盘演示数据量不大用户也不多数据库就装在同一台电脑或局域网服务器上客户端直连部署最省事。第三类是离线或单机应用的“伪需求”很多团队喜欢先把功能跑起来等用户量上来了再做服务端那么前期用直连方案可以极大降低开发成本等需要扩展时再换连接点。当然直连方案也有明显缺点数据库账号暴露在客户端里、连接安全性差、无法承载高并发。如果你的项目会发布到公网请务必不要这么干老老实实走后端接口。我用直连主要看中的是“交付快、改动小、本地可控”这些在工具和内部系统里恰恰是最值钱的。1.2 为什么选 FreeSql而不是 EF Core 或 Dapper在 .NET 世界里做 ORM能选的无非是 EF Core、Dapper、SqlSugar、FreeSql 这几个。我最初也纠结过说说我的取舍逻辑。EF Core 功能强大但依赖的组件多在 Unity 里导入很折腾而且有时为了支持 IL2CPP 要做额外裁剪配置。Dapper 很轻但本质是扩展方法加手写 SQL我要自己维护实体映射也不够“省事”。SqlSugar 也不错但社区和文档更偏向 .NET 服务端。FreeSql 对我来说最大的吸引力是三点一是 API 简单直接一个 IFreeSql 对象就覆盖了增删改查、仓储、事务、分页几乎没有学习门槛二是官方明确支持 .NET Standard 2.0/2.1Unity 2020 以上版本可以直接装 dll 用三是中文文档完整遇到问题可以很快搜到解决方案。作为一个以 Unity 为主的技术栈FreeSql 算是最“顺滑”的选择。当然选型没有绝对好坏如果你的团队已经熟 EF Core或者项目确定只用 Dapper继续用也没问题。我这里分享的方案重点是“直连 MySQL ORM 一体化”FreeSql 只是实现这个思路的顺手工具。1.3 方案的整体流程与边界整体流程很清晰Unity 客户端通过 NuGet 或其他方式引入 FreeSql 及其 MySQL 驱动MySqlConnector在启动时创建 IFreeSql 单例用它来加载实体类对应的数据库表。之后所有操作都走仓储接口最终由 FreeSql 翻译成 SQL 发给 MySQL 执行。需要注意边界直连 MySQL 是同步 IO在 Unity 主线程直接调用会卡 UI。所以生产级代码应该用异步方法或协程把数据库操作放到后台线程。另外移动平台打包后需要对 MySQL 服务器的端口开放、防火墙规则、SSL 配置做额外处理这些我在第 4 节会专门讲。明确了这些边界后面的实现才不会跑偏。2. 核心细节解析与实操要点2.1 Unity 环境与依赖库导入我用的是 Unity 2021.3 LTS加上 .NET Standard 2.1 作为 Api Compatibility Level。如果你还是老版本至少也要 Unity 2019因为太老的 .NET 4.x 对 MySqlConnector 支持不够。导入 FreeSql 有三种方式我推荐用 NuGetForUnity。参考步骤先装 NuGetForUnity 插件直接从 GitHub 仓库下载或用 Git URL 导入。在 Unity 菜单栏打开 NuGet → Manage Packages。搜索 FreeSql安装最新稳定版。FreeSql 会自动拉取 MySqlConnector 等依赖。如果不用 NuGetForUnity也可以手动从 NuGet 包里把 FreeSql.dll、MySqlConnector.dll 拖进 Assets/Plugins 文件夹。安装完成后检查FreeSql.dll和MySqlConnector.dll是否出现在工程里。如果引用报错多半是 Api Compatibility Level 太低建议改成.NET Framework或.NET Standard 2.1。我个人不建议用 IL2CPP .NET 4.x 的旧组合后面打包问题会特别多。提示如果在 NuGetForUnity 里搜索不到 FreeSql可以检查是否设置了 NuGet 源为 nuget.orgWindows 平台经常因为代理或缓存导致列表加载不全。2.2 实体类与表结构设计ORM 的核心就是把数据库表映射成 C# 类。我拿一张玩家表来举例结构如下using System; using FreeSql.DataAnnotations; [Table(Name player)] public class Player { [Column(IsPrimary true, IsIdentity true)] public int Id { get; set; } [Column(Name name)] public string Name { get; set; } [Column(Name level)] public int Level { get; set; } [Column(Name create_time)] public DateTime CreateTime { get; set; } }字段命名和使用习惯尽量保持一致表名用小写字段统一 snake_caseC# 属性用 PascalCase用[Column(Name...)]做映射。这样后端的 DBA 看着舒服你写代码也不会犯迷糊。这里要特别提醒[Column(IsPrimary true, IsIdentity true)]声明了主键自增。如果你的表主键不是自增而是业务 ID就别加IsIdentity插入时手工赋值。FreeSql 的自动同步建表功能会读这些特性但生产环境我一般关闭自动同步只在开发期临时打开。2.3 连接串与 FreeSql 初始化FreeSql 的初始化很简单但连接串是第一个坑。我用的是这样的模板static Db() { Fsql new FreeSqlBuilder() .UseConnectionString(DataType.MySql, Server127.0.0.1;Port3306;Databasegame_demo;Uidroot;Pwd123456;Charsetutf8mb4;SslModeNone;) .UseAutoSyncStructure(false) .UseMonitorCommand(cmd Debug.Log($SQL: {cmd.CommandText})) .Build(); }几个关键点Server和Port是 MySQL 的地址与端口默认 3306远程服务器要填公网 IP 或内网 IP。Charsetutf8mb4很关键否则中文可能乱码。utf8mb4 是 MySQL 对四字节 emoji 的支持方案比 utf8 更全面。SslModeNone是针对本机或内网的常用做法因为很多 MySQL 默认配置证书不可用直接 SSL 握手会失败。公网环境则建议改为Preferred或配置证书。UseMonitorCommand可以把 FreeSql 生成的 SQL 打印到 Unity 的 Console调试增删改查时是神器。初始化好的IFreeSql是线程安全的可以做成静态单例供整个工程任意地方调用。千万不要每次操作都new FreeSqlBuilder()否则连接池会被耗尽。2.4 仓储模式的封装思路直接用IFreeSql也能读写但更推荐用仓储接口。FreeSql 的GetRepositoryT()返回一个IBaseRepositoryT内置增删改查和分页比裸用 raw SQL 舒服得多。我在项目里封装了一个通用基类类似于public class BaseRepositoryT where T : class { protected IBaseRepositoryT Repo Db.Fsql.GetRepositoryT(); }然后每个业务类继承它。好处是以后想加缓存、审计日志只需改基类不用动业务代码。如果项目特别简单直接用Db.Fsql.GetRepositoryPlayer()也可以前面代码都按这个思路。3. 实操过程与核心环节实现现在进入正题。下面所有示例都基于Db.Fsql.GetRepositoryPlayer()这个仓储对象我给它命名为repo。3.1 增插入一条玩家记录插入数据最简单直接Insert一个实体var player new Player { Name 阿伟, Level 1, CreateTime DateTime.Now }; repo.Insert(player); Debug.Log($新增玩家ID: {player.Id});Insert后FreeSql 会自动把自增主键回填到player.Id这个细节很实用。如果主键不是自增请手动赋值否则会报错。批量插入也不难var list new ListPlayer(); for (int i 0; i 100; i) { list.Add(new Player { Name $玩家{i}, Level Random.Range(1, 100) }); } repo.Insert(list);实际项目里插入前最好检查一下唯一字段是否重复避免主键或唯一索引冲突。也可以在实体上建唯一索引但异常处理还是要做。批量插入数据量大的时候可以进一步用事务包起来避免插入一半失败导致脏数据。3.2 删按条件删除记录删除操作我常用两种姿势// 方式一删除实体 repo.Delete(player); // 方式二按条件删除 repo.Delete(p p.Id 1024);Delete(player)会根据主键删除要求实体主键有值。按条件删除更灵活可以写多个条件比如Delete(p p.Level 0 p.CreateTime DateTime.Now.AddDays(-30))。FreeSql 删除还有一个特点是它会把条件翻译成参数化 SQL不像字符串拼接那样容易注入。但条件表达式里如果有函数调用比如DateTime.Now它会在 C# 侧先求值再作为参数传入所以不会出现“语法错误”。不过要注意大量数据删除前最好先Select查一下条数防止误删太多数据。3.3 改更新玩家等级更新是最容易出 bug 的地方因为很多人会不经意把整个实体所有字段都 Update 一遍。如果你只要改等级推荐用UpdateDiyrepo.UpdateDiy .Set(a a.Level, 100) .Where(a a.Id 5) .ExecuteAffrows();这句意思是“只把 ID 为 5 的玩家的 Level 更新为 100”生成的 SQL 只有UPDATE player SET level 100 WHERE id 5不会动其他字段。如果是先查到实体、再整体更新可以这样var p repo.Where(a a.Id 5).First(); p.Level 99; repo.Update(p);但注意repo.Update(p)会更新所有字段包括 Name、CreateTime。如果这些字段被别的线程改过可能覆盖数据所以“查出来改一个字段”的场景尽量用UpdateColumnsrepo.UpdateDiy .SetSource(p) .UpdateColumns(a new { a.Level }) .ExecuteAffrows();SetSource指定数据源UpdateColumns指定只更新哪几列比较安全。这也是我在团队里要求大家尽量遵守的规则能谓词更新就不要全量更新减少并发冲突。3.4 查列表、单条、分页与条件查询查询是重头戏。FreeSql 的Select方法返回一个查询对象支持连缀// 查询大于等于10级的所有玩家 var list repo.Select .Where(a a.Level 10) .OrderByDescending(a a.Level) .ToList();单条查询var one repo.Where(a a.Id 1).First();分页查询var page repo.Select .Where(a a.Level 0) .OrderByDescending(a a.Level) .Page(1, 20) // 第1页每页20条 .ToList();如果要拿到总条数和总页数可以这样var count repo.Select.Where(a a.Level 0).Count(); var list repo.Select .Where(a a.Level 0) .OrderByDescending(a a.Level) .Page(1, 20) .ToList();这里有一个隐藏优化Page方法在 MySQL 底层翻译为limit/offset数据量大时记得在表上建好索引。没有索引的话查询会全表扫描卡到怀疑人生。FreeSql 还支持多表联查SelectT, T2之类的用法但 Unity 里我不建议把关联做太复杂宁可拆成多次查询在 C# 里组装。原因很简单客户端直连数据库复杂 SQL 出现问题后排查成本比服务端高得多。保持查询简单是客户端数据库操作的第一原则。3.5 事务与批量操作如果一次操作要同时更新多张表或者“增 改”必须保证原子性就要用事务。using (var trans Db.Fsql.BeginTransaction()) { try { var repo Db.Fsql.GetRepositoryPlayer(); repo.Insert(new Player { Name 事务测试, Level 1, CreateTime DateTime.Now }); repo.UpdateDiy .Set(a a.Level, a a.Level 1) .Where(a a.Id 100) .ExecuteAffrows(); trans.Commit(); } catch (Exception e) { Debug.LogError($事务回滚: {e.Message}); trans.Rollback(); } }事务是连接级别的Unity 里如果同时有多个协程在操作数据库务必确保事务里的操作都在同一线程或同一连接上执行否则会碰到“连接已被占用”的报错。我在项目里会用一个简单的数据库操作调度器把所有数据库命令按顺序排队避免跨线程交叉。简单做法就是加一个互斥锁复杂一些就用队列加消费者线程。4. 常见问题与排查技巧实录4.1 IL2CPP 打包后反射被裁剪Unity 打包 Android/iOS 时默认使用 IL2CPP会把没用到的反射代码裁掉。FreeSql 很多底层动态生成实体和 SQL被误裁后就会报MissingMethodException或FileNotFound。解决办法是在工程里增加一个link.xml把 FreeSql 和 MySqlConnector 相关程序集排除linker assembly fullnameFreeSql preserveall / assembly fullnameFreeSql.DbContext preserveall / assembly fullnameMySqlConnector preserveall / /linker放在Assets目录下打包时 Unity 会自动读取。如果还不行就把实体类和事件相关的类型也加进去通常能解决。这个坑我当初踩的时候很痛因为编辑器里跑得好好的一打 Android 包就崩排查了一整天最后发现就是link.xml的问题。4.2 TLS/SSL 协议不兼容默认情况下 MySqlConnector 会要求 MySQL 服务器支持 TLS 1.2。如果你用的是老版本 MySQL比如 5.6或系统 OpenSSL 组件不全会报类似 “SSL Connection Error” 的错误。最简单的排查方法是先绕开 SSLSslModeNone;如果业务要求必须加密传输就要检查服务器端 SSL 证书是否有效以及客户端系统的证书信任链是否完整。Windows 上跑没问题、Android 上报错多半是证书链问题。移动端对证书校验比较严格自签名证书经常会挂所以内网工具我干脆关闭 SSL省得折腾。4.3 移动端网络权限与防火墙Android 打包后连不上本机数据库大概率是权限和网络安全配置没写。AndroidManifest.xml 里要加uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /Android 9 以上默认禁止明文 HTTP 流量MySQL 直连使用的是 TCP 裸协议不算 HTTP但部分厂商会拦截。如果你用的是自定义端口也要检查服务器防火墙是否放行该端口。iOS 上则要配置 ATSApp Transport Security允许任意加载的例外。局域网联调时我习惯先用手机浏览器访问http://服务器IP:3306测试通不通虽然浏览器不会真的完成 MySQL 协议但至少能判断端口是否被拦截。如果端口不通优先查服务器防火墙和路由器端口转发别一头扎进代码里调。4.4 中文乱码与字符集插入中文后读出来是问号十有八九是连接串字符集不对或者表/字段字符集不是 utf8mb4。先检查连接串必须带Charsetutf8mb4。再检查 MySQL 表结构SHOW CREATE TABLE player;看一下表的DEFAULT CHARSET是不是utf8mb4排序规则最好是utf8mb4_general_ci或utf8mb4_unicode_ci。如果建表时没指定可以修改已有表ALTER TABLE player CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;改完之后重启 Unity 程序再测别只在数据库工具里看因为工具已经缓存了旧编码。乱码问题通常不会出现在编辑器模式反而在真机上更容易遇到所以真机联调时要特别留意。4.5 主线程阻塞与异步操作这条很关键。Unity 的渲染和 UI 更新都在主线程如果直接在按钮点击回调里执行同步 CRUD当数据库卡顿或网络超时时整个画面会冻结好几秒。更稳妥的用法是配合 C# 的 async/await。FreeSql 提供了异步版本async void OnClick_Query() { var list await repo.Select .Where(a a.Level 10) .ToListAsync(); foreach (var p in list) { Debug.Log($玩家{p.Name}, 等级{p.Level}); } }注意async void只能用于事件回调。如果是在普通方法里用async Task更规范。异步操作仍然要避免并发冲突我用一个简单的信号量SemaphoreSlim去限制同一时间只执行一个数据库操作实测很稳。当然最省心的还是把数据库操作封装到一个独立线程里通过 Unity 主线程的SynchronizationContext回传结果不过那套代码会复杂不少新手先用 async/await 就够了。4.6 常见异常速查表我整理了几个高频出现的问题方便你直接对照异常现象可能原因解决办法Authentication method caching_sha2_password not supportedMySQL 8 默认认证插件较新改成mysql_native_password或升级 MySqlConnector 到最新版Unable to connect to any of the specified MySQL hosts网络不通 / 防火墙检查 IP、端口测试 telnet 连接Table game_demo.player doesnt exist没有建表或自动同步被关闭开启UseAutoSyncStructure(true)先建一次表再决定是否关闭Timeout expired连接池不足 / SQL 太慢加大连接串里Connection Timeout优化 SQL保证索引Column level cannot be null传入的实体字段值为 null检查实体赋值避免 null 写入非空字段Packets out of order网络环境差 / 连接被中间设备干扰关闭 SSL或改用长连接并做重连看到异常列表不要慌先把日志级别调到 Verbose看 FreeSql 打印出的 SQL大多数问题一眼就能定位。如果实在定位不了用数据库工具手动执行一遍同款 SQL对比结果就能找到差异。这块内容做到最后我最大的感受是“直连数据库”并没有想象中那么可怕前提是清楚项目边界并做好防御。如果你开发的是内部工具、演示系统或者想让 Unity 客户端快速拥有持久化能力FreeSql MySQL 确实是一套能快速上手的组合。但如果你的目标是公网产品请务必把数据库藏到后端客户端只走 API别拿用户数据开玩笑。最后再分享一个小技巧开发时打开UseMonitorCommand打印 SQL排查问题能省一半时间上线前记得关掉别把 SQL 全部暴露在日志里。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询