Unity直连MySQL:用FreeSql实现高效CRUD与避坑指南
2026/9/18 2:56:08 网站建设 项目流程

在 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.1,Unity 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。参考步骤:

  1. 先装 NuGetForUnity 插件(直接从 GitHub 仓库下载或用 Git URL 导入)。
  2. 在 Unity 菜单栏打开 NuGet → Manage Packages。
  3. 搜索 FreeSql,安装最新稳定版。FreeSql 会自动拉取 MySqlConnector 等依赖。
  4. 如果不用 NuGetForUnity,也可以手动从 NuGet 包里把 FreeSql.dll、MySqlConnector.dll 拖进 Assets/Plugins 文件夹。

安装完成后,检查FreeSql.dllMySqlConnector.dll是否出现在工程里。如果引用报错,多半是 Api Compatibility Level 太低,建议改成.NET Framework.NET Standard 2.1。我个人不建议用 IL2CPP + .NET 4.x 的旧组合,后面打包问题会特别多。

提示:如果在 NuGetForUnity 里搜索不到 FreeSql,可以检查是否设置了 NuGet 源为 nuget.org,Windows 平台经常因为代理或缓存导致列表加载不全。

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_case,C# 属性用 PascalCase,用[Column(Name=...)]做映射。这样后端的 DBA 看着舒服,你写代码也不会犯迷糊。

这里要特别提醒:[Column(IsPrimary = true, IsIdentity = true)]声明了主键自增。如果你的表主键不是自增,而是业务 ID,就别加IsIdentity,插入时手工赋值。FreeSql 的自动同步建表功能会读这些特性,但生产环境我一般关闭自动同步,只在开发期临时打开。

2.3 连接串与 FreeSql 初始化

FreeSql 的初始化很简单,但连接串是第一个坑。我用的是这样的模板:

static Db() { Fsql = new FreeSqlBuilder() .UseConnectionString(DataType.MySql, "Server=127.0.0.1;Port=3306;Database=game_demo;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;") .UseAutoSyncStructure(false) .UseMonitorCommand(cmd => Debug.Log($"SQL: {cmd.CommandText}")) .Build(); }

几个关键点:

  • ServerPort是 MySQL 的地址与端口,默认 3306,远程服务器要填公网 IP 或内网 IP。
  • Charset=utf8mb4很关键,否则中文可能乱码。utf8mb4 是 MySQL 对四字节 emoji 的支持方案,比 utf8 更全面。
  • SslMode=None是针对本机或内网的常用做法,因为很多 MySQL 默认配置证书不可用,直接 SSL 握手会失败。公网环境则建议改为Preferred或配置证书。
  • UseMonitorCommand可以把 FreeSql 生成的 SQL 打印到 Unity 的 Console,调试增删改查时是神器。

初始化好的IFreeSql是线程安全的,可以做成静态单例,供整个工程任意地方调用。千万不要每次操作都new FreeSqlBuilder(),否则连接池会被耗尽。

2.4 仓储模式的封装思路

直接用IFreeSql也能读写,但更推荐用仓储接口。FreeSql 的GetRepository<T>()返回一个IBaseRepository<T>,内置增删改查和分页,比裸用 raw SQL 舒服得多。

我在项目里封装了一个通用基类,类似于:

public class BaseRepository<T> where T : class { protected IBaseRepository<T> Repo => Db.Fsql.GetRepository<T>(); }

然后每个业务类继承它。好处是以后想加缓存、审计日志,只需改基类,不用动业务代码。如果项目特别简单,直接用Db.Fsql.GetRepository<Player>()也可以,前面代码都按这个思路。

3. 实操过程与核心环节实现

现在进入正题。下面所有示例都基于Db.Fsql.GetRepository<Player>()这个仓储对象,我给它命名为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 List<Player>(); 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 一遍。如果你只要改等级,推荐用UpdateDiy

repo.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。如果这些字段被别的线程改过,可能覆盖数据,所以“查出来改一个字段”的场景,尽量用UpdateColumns

repo.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 还支持多表联查,Select<T, T2>之类的用法,但 Unity 里我不建议把关联做太复杂,宁可拆成多次查询,在 C# 里组装。原因很简单:客户端直连数据库,复杂 SQL 出现问题后,排查成本比服务端高得多。保持查询简单,是客户端数据库操作的第一原则。

3.5 事务与批量操作

如果一次操作要同时更新多张表,或者“增 + 改”必须保证原子性,就要用事务。

using (var trans = Db.Fsql.BeginTransaction()) { try { var repo = Db.Fsql.GetRepository<Player>(); 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,被误裁后就会报MissingMethodExceptionFileNotFound

解决办法是在工程里增加一个link.xml,把 FreeSql 和 MySqlConnector 相关程序集排除:

<linker> <assembly fullname="FreeSql" preserve="all" /> <assembly fullname="FreeSql.DbContext" preserve="all" /> <assembly fullname="MySqlConnector" preserve="all" /> </linker>

放在Assets目录下,打包时 Unity 会自动读取。如果还不行,就把实体类和事件相关的类型也加进去,通常能解决。这个坑我当初踩的时候很痛,因为编辑器里跑得好好的,一打 Android 包就崩,排查了一整天,最后发现就是link.xml的问题。

4.2 TLS/SSL 协议不兼容

默认情况下 MySqlConnector 会要求 MySQL 服务器支持 TLS 1.2。如果你用的是老版本 MySQL(比如 5.6)或系统 OpenSSL 组件不全,会报类似 “SSL Connection Error” 的错误。

最简单的排查方法是先绕开 SSL:

"SslMode=None;"

如果业务要求必须加密传输,就要检查服务器端 SSL 证书是否有效,以及客户端系统的证书信任链是否完整。Windows 上跑没问题、Android 上报错,多半是证书链问题。移动端对证书校验比较严格,自签名证书经常会挂,所以内网工具我干脆关闭 SSL,省得折腾。

4.3 移动端网络权限与防火墙

Android 打包后连不上本机数据库,大概率是权限和网络安全配置没写。AndroidManifest.xml 里要加:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Android 9 以上默认禁止明文 HTTP 流量,MySQL 直连使用的是 TCP 裸协议,不算 HTTP,但部分厂商会拦截。如果你用的是自定义端口,也要检查服务器防火墙是否放行该端口。iOS 上则要配置 ATS(App Transport Security),允许任意加载的例外。

局域网联调时,我习惯先用手机浏览器访问http://服务器IP:3306测试通不通,虽然浏览器不会真的完成 MySQL 协议,但至少能判断端口是否被拦截。如果端口不通,优先查服务器防火墙和路由器端口转发,别一头扎进代码里调。

4.4 中文乱码与字符集

插入中文后读出来是问号,十有八九是连接串字符集不对,或者表/字段字符集不是 utf8mb4。

先检查连接串:必须带Charset=utf8mb4。再检查 MySQL 表结构:

SHOW CREATE TABLE player;

看一下表的DEFAULT CHARSET是不是utf8mb4,排序规则最好是utf8mb4_general_ciutf8mb4_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' doesn't 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 全部暴露在日志里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询