写SqlSugar的人很大概率都已经过了“能用就行”的阶段——毕竟真到了用C#操作MySQL做项目,说明你至少已经尝试过EF Core、Dapper,甚至可能被ADO.NET折磨过一轮。SqlSugar的定位很讨巧:它比ADO.NET省心,比EF Core轻量,API设计又比Dapper更贴近业务开发者的直觉。但这两年我拿SqlSugar做了好几个MySQL项目,从开发调试到线上救火,踩过的坑比文档里写出来的多得多。
这篇文章不聊安装配置,不复制文档,只说我实际操作中遇到过的、容易忽略但后果严重的5个细节。适合已经在用SqlSugar、或者正准备在C#项目里接MySQL的开发者参考,尤其是那种“开发环境一切正常、一到生产环境就翻车”的场景,你应该能从里面找到自己的影子。
1. 表名映射与大小写:Windows开发没问题,Linux部署就报找不到表
我在2023年接手过一个WMS仓储管理系统,技术栈就是C# + SqlSugar + MySQL。开发阶段大家用的都是Windows,代码里实体类写的是WarehouseReceipt、InventoryRecord,SqlSugar默认命名策略会把驼峰实体名转成下划线小写表名,所以在本地跑得顺风顺水。结果一部署到Linux服务器上的MySQL,系统直接起不来,报错特别抽象:Table 'db_xxx.InventoryRecord' doesn't exist。
你以为是自己SQL写错了,查了半天才发现是大小写的问题。
1.1 MySql大小写敏感的真正成因
MySQL在Linux下默认表名是区分大小写的,具体的开关是系统参数lower_case_table_names,取值含义:
| 参数值 | 行为说明 | 常见系统 |
|---|---|---|
| 0 | 表名按指定大小写存储,比较时区分大小写 | Linux默认 |
| 1 | 表名以小写存储,比较时不区分大小写 | Windows默认 |
| 2 | 表名按指定大小写存储,比较时不区分大小写 | macOS默认 |
Windows上你建了InventoryRecord,SQL写inventoryrecord也能查到,因为Windows的MySQL默认lower_case_table_names=1,它会在内部统一转小写。但Linux上默认是0,InventoryRecord和inventory_record就是两个完全不同的表。
那这和SqlSugar有什么关系?关系很大。SqlSugar默认建表时会执行一个“实体名转表名”的策略——WarehouseReceipt默认映射成warehouse_receipt。如果你项目里用的是SqlSugar的CodeFirst自动建表,那数据库里存的就是小写下划线表名,后续查询SqlSugar也会自动加上反引号去查小写表名,这套逻辑自洽,不会出问题。问题是绝大多数项目并不是纯SqlSugar建表,而是先用Navicat、MySQL Workbench或者DBA提供的SQL脚本把表建好,然后你再写实体类去映射。
我那个WMS项目的症结正是这个:数据库脚本是DBA写的,表名是驼峰原样建的,实体类也是驼峰原样写的,本地Windows查询不区分大小写,所以一切正常。一上Linux,SqlSugar查询时把实体名InventoryRecord按默认策略转成了小写下划线名inventory_record,但库里实际表名是InventoryRecord,自然就报找不到表了。
1.2 统一的处理方案
排查这个问题第一步,先用这条SQL确认当前数据库的参数状态:
SHOW VARIABLES LIKE 'lower_case_table_names';如果是Linux且值为0,而你又不想改动数据库服务器配置(因为改这个参数需要重启MySQL,而且会影响存量表,生产环境风险很大),那就别指望改服务器了,应该在连接层面解决。
在创建SqlSugar实例时,显式关闭自动下划线转换:
var db = new SqlSugarClient(new ConnectionConfig { ConnectionString = "Server=127.0.0.1;Port=3306;Database=wms;Uid=root;Pwd=123456;", DbType = DbType.MySql, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute, ConfigureExternalServices = new ConfigureExternalServices { EntityService = (property, column) => { // 不让SqlSugar自动把属性名转下划线,保持和数据库一致 column.DbColumnName = property.Name; } } });对实体的表名,用特性强制锁定:
[SugarTable("InventoryRecord")] public class InventoryRecord { [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] public long Id { get; set; } [SugarColumn(ColumnName = "WarehouseCode")] public string WarehouseCode { get; set; } }这样SqlSugar在生成SQL时就不会自作主张转表名和字段名了。说句实话,这种方案虽然丑,但最稳妥,也能让你不用动DBA那边的脚本。如果你的项目是从零开始、表结构完全由SqlSugar管理,那其实保持默认下划线策略反而是最优解,最怕的就是“一部分表手动建、一部分表CodeFirst建”的混合模式,这种项目先天就埋雷。
提示:用Navicat的“模型”功能建表时,表名建议直接用全小写下划线命名,实体类用PascalCase命名并在实体上打
[SugarTable("tinyint_table")]这种显式映射,两个世界的规则就统一了。很多团队“上线即翻车”的故障,百分之八十都是命名策略没对齐。
2. 批量插入慢只是表象,真正的坑是参数数量和事务边界
我见过太多写SqlSugar的人,批量插入还是这个搞法:
foreach (var item in list) { db.Insertable(item).ExecuteCommand(); }数据量小无所谓,一条一条插也就几十毫秒。可一旦单次要插入的数据量到了几千上万条,这个写法就是一场灾难。我有一个项目做库存初始化导入,Excel里1.8万条记录,用循环插入跑了将近8分钟,用户差点以为程序死了。
2.1 性能差异到底有多大
我专门做过一组简单测试,环境是Windows 10 + MySQL 8.0,单表10个字段,插入1万条数据,分别用三种方式执行:
| 方式 | 执行耗时 | 说明 |
|---|---|---|
| 循环单条Insertable | 约90秒 | 每条一次网络往返,有连接开销 |
| Insertable(list).ExecuteCommand() | 约4.5秒 | 多个值拼接成一条Insert语句,但MySQL参数数量有限制 |
| MySqlBulkCopy | 约1.2秒 | 走MySQL原生LOAD DATA,性能最稳 |
看到这个对比,大部分人会立刻换到第二种写法,因为代码几乎不用改多少。但第二个坑就在这等着呢。
2.2 MySqlBulkCopy的正确用法
我后来在另一个项目中用了MySqlBulkCopy,它是MySQL官方连接器MySqlConnector带的类,可以把DataTable直接灌进数据库。记得先装NuGet包:
Install-Package MySqlConnector代码思路很简单:先把实体列表转成DataTable,列名必须和目标表字段完全一致,然后调用MySqlBulkCopy的WriteToServer方法。
public static void BulkInsert<T>(string connectionString, string tableName, List<T> list, int batchSize = 10000) { DataTable dt = new DataTable(); // 这里假设实体属性与表字段一一对应,实际项目建议用反射遍历属性构建列 dt.Columns.Add("Id", typeof(long)); dt.Columns.Add("WarehouseCode", typeof(string)); dt.Columns.Add("Quantity", typeof(decimal)); // ... foreach (var item in list) { dt.Rows.Add( typeof(T).GetProperty("Id")?.GetValue(item), typeof(T).GetProperty("WarehouseCode")?.GetValue(item), typeof(T).GetProperty("Quantity")?.GetValue(item) ); } using var conn = new MySqlConnection(connectionString); conn.Open(); using var bulk = new MySqlBulkCopy(conn) { DestinationTableName = tableName, BulkCopyTimeout = 300 }; bulk.WriteToServer(dt); }这里有个容易踩的坑:批处理大小不是越大越好。max_allowed_packet限制了单次网络包的大小,如果数据行字段特别多、字段内容又长,一次塞太多行反而会报PacketTooLargeException。我实践下来,1万行一批,单行平均字段长度在50字节以内是安全的。如果单行有text字段,建议降到2000行一批。
2.3 批量插入是否开启事务的判断标准
SqlSugar的Insertable(list).ExecuteCommand()并不会自动开启事务,它是一条SQL语句,天然具备原子性。因为单条插入SQL要么全部成功要么全部失败,不需要你额外包事务。
但如果你拼接的批量SQL因为参数过多,被迫拆分成多个批次执行,那就要小心了——拆分之后每个批次独立成功,如果中间某个批次失败,前面批次的数据已经在库里了,但你的业务逻辑可能认为“导入失败”,下次重试就会产生重复数据。
我的处理原则是:
- 总数据量小于5000条,直接用
Insertable(list).ExecuteCommand()一把梭(前提是参数数量别超过MySQL限制,后面会细说)。 - 数据量在5000条到50000条之间,用
MySqlBulkCopy,它内部有事务控制,出错会回滚当前批次。 - 超过5万条,分批次加事务,每批次结束提交,并做好断点记录,方便失败后从断点续传。
3. 日期时间的时区问题:开发机正常,服务器上时序数据全错位
你敢信一个物联网设备温度监测项目,最后排查出的问题是服务器上的所有时间都差了8小时?我印象很深,2024年初做的一个冷链运输监控系统,一批冷柜温度数据每隔5分钟上报一次,客户反馈“凌晨的数据跑到下午去了”。一开始以为传感器上报逻辑出了问题,查了两三天,最后发现是C#的DateTime.Now和MySQL时区搞出来的乌龙。
3.1 问题是怎么发生的
项目里实体类定义的时间字段是DateTime,插入数据时用的是DateTime.Now。开发机上跑一切正常,因为开发机是CST时间(东八区),MySQL也是CST时间,两边一致。但服务器上情况变了:很多云服务器默认时区是UTC,MySQL也沿用UTC,而C#程序的DateTime.Now返回的是操作系统本地时间。如果服务器操作系统是UTC,那DateTime.Now拿到的就是UTC时间。数据库存的看似没问题,应用读出来好像是正常时间,可一旦有别的程序或者人直接去MySQL里看数据,所有时间都比北京时间差了8小时。
更隐蔽的坑是:如果你有两个不同时区的服务器都在往同一个数据库写数据,那么同一张表里的时间就会乱成一锅粥,有的记录是UTC、有的是CST,你永远不知道某一条数据到底是什么时刻发生的。
3.2 统一的解决方案
这里不讨论“该用UTC还是本地时间”的架构问题,只说我验证后最稳的做法:数据库统一存UTC,应用层负责换算展示。
首先,连接字符串里加上时区设置:
Server=127.0.0.1;Port=3306;Database=coldchain;Uid=root;Pwd=123456;SslMode=none;ConnectionTimeout=30;CharacterSet=utf8mb4;AllowZeroDateTime=True;注意,MySqlConnector默认会有一个转换规则,它会把MySQL的DATETIME类型读成DateTimeKind.Unspecified,把TIMESTAMP类型读成DateTimeKind.Local,这就导致你从数据库里读出来的同一个DateTime值,不同字段的Kind还不一样,做时间比较和格式化的时候就会出现诡异的差别。
实体字段建议统一指定:
[SugarColumn(ColumnDataType = "datetime(6)")] public DateTime UtcTime { get; set; }写入时统一用DateTime.UtcNow,读取展示时再转本地时间:
var localTime = record.UtcTime.ToLocalTime();这里要提醒一点:实体字段如果是DateTime直接映射MySQL的datetime,毫秒会被丢弃,因为MySQL老版本datetime精度只到秒。如果你需要毫秒级时间戳,列类型必须显式写成datetime(6)。否则你从数据库读回来的时间精度会诡异地从“带毫秒”变成“整秒”,做数据对比的时候可能无缘无故多出几毫秒的差。
我建议所有时间敏感的表字段全部用datetime(6),没有例外。查询性能的损失可以忽略不计,但时间精度的坑一旦踩中,排错成本极高。
提示:排查时间类问题,先看三处:C#代码里用的是
DateTime.Now还是DateTime.UtcNow;MySQL的time_zone变量是什么;实体字段类型是不是datetime(6)。如果三层都一致,时间问题基本不会出现。
4. 类型映射的隐性问题:bool变byte、int变long、decimal丢精度
在用SqlSugar写查询时,大部分人习惯用db.Queryable<T>()把查询结果直接映射到实体,这种强类型映射几乎没有类型转换问题。但一旦你用了原生SQL、DataTable、或者List<dynamic>接收结果,SqlSugar的“贴心的类型推断”就会给你带来一连串小麻烦,而且都是那种看起来不大、但会把你折腾半天的怪问题。
4.1 MySQL类型与C#类型最常见的错位
| MySQL列类型 | SqlSugar映射实体类型 | 原生SQL/DataTable读出的实际类型 | 潜在问题 |
|---|---|---|---|
| tinyint(1) | bool | byte | 直接用DataTable取值再转bool会抛异常 |
| int / bigint | int / long | long(不管原始定义) | 实体映射短整型,读取无碍;但DataTable里取int会拿到long,不显式转换就报错 |
| decimal(18,4) | decimal | decimal | 实体映射没问题,但求和、平均值SQL返回可能被推断成decimal(38,4)或double |
| json | string | string | 直接读取是字符串,需要自己反序列化 |
| datetime(6) | DateTime | DateTime | 如果没映射好,可能拿到DateTime或MySqlDateTime对象,行为有差异 |
| text/longtext | string | string | 一般没坑,但要注意字符集 |
最常见的坑出现在联表查询里。我用db.Queryable<Order>().LeftJoin<OrderItem>((o, oi) => new JoinQueryInfos(o.Id, oi.OrderId))查出来的DTO里如果有个int Count属性,SqlSugar在做数据填充的时候,底层拿到的可能是一个long,然后它做一些宽松转换。大部分时候没问题,但如果你用了Select<T>()做自定义投影,把数据库的SUM()、COUNT()结果赋给一个int属性,就会偶发InvalidCastException——因为MySQL的聚合函数返回类型是DECIMAL或BIGINT,和int不是直接兼容的。
4.2 实战避坑经验
如果你必须用DataTable接收原生SQL结果,建议养成一个习惯:取值时不要直接用(bool)row["IsActive"]这种强转,而是用Convert.ToBoolean、Convert.ToInt32这种方式,它会自动处理底层类型变化:
DataTable dt = db.Ado.GetDataTable("SELECT Id, IsActive, Quantity FROM orders WHERE create_time > @time", new { time = startTime }); foreach (DataRow row in dt.Rows) { bool isActive = Convert.ToBoolean(row["IsActive"]); int qty = Convert.ToInt32(row["Quantity"]); }Convert系列方法本质上就是在做类型跳转的兜底,虽然性能比直接强转略低一点点,但对业务系统来说这点损耗可以忽略。加using System;,Convert就有。
如果是用db.Ado.SqlQuery<T>去拿DTO,注意DTO属性类型和SQL结果类型尽量完全一致。我做数据看板时,为了省事直接在SQL里写SUM(o.amount) AS TotalAmount,然后DTO里用decimal TotalAmount来接,结果在某些MySQL版本下返回的是double类型,反序列化直接炸了。
后来我养成了习惯——所有聚合查询的SQL里,显式指定CAST:
SELECT CAST(SUM(o.amount) AS DECIMAL(18,2)) AS TotalAmount FROM orders o三个字:别偷懒。SQL里少写一个CAST,C#里可能要多调半天。
4.3 bool字段在实体里的正确姿势
SqlSugar映射MySQL的bool字段,对应的数据库类型是tinyint(1)。这个在实体类直接映射没问题,但如果你打算在实体里用bool、数据库列却是tinyint(4),那就要非常小心。tinyint(4)会被映射成byte或sbyte,而不是bool。你如果在实体属性上写了bool,SqlSugar执行查询时不会报错,但读取时可能返回非0/1的值(运营人员手动改数据很容易干这种事),Convert.ToBoolean遇到数值2会怎样?
Convert.ToBoolean(2)是返回true的,没问题。但如果你不巧用(bool)reader["flag"]这种强转方式去取值,value是byte类型的2,强转bool直接抛InvalidCastException。所以还是那句话:要么严格在SQL里写好转换,要么用Convert到底。
5. 分页、多表Update和原生SQL,属于看起来简单、翻车率最高的三个场景
SqlSugar的分页API很舒服——ToPageList(pageIndex, pageSize),底层自动根据数据库类型生成分页SQL。MySQL下的分页SQL长这样:
SELECT * FROM orders ORDER BY create_time DESC LIMIT 20, 10;LIMIT的偏移量是(pageIndex - 1) * pageSize。这个逻辑本身没问题,但真正踩坑的是一个很反直觉的点:当你用了OrderBy分页,并且排序的字段有重复值时,MySQL的分页结果可能重复或漏数据。
5.1 分页结果重复/丢失,是因为排序字段不唯一
举个例子,如果按create_time倒序分页,但同一秒内创建了10条订单,那么第一页和第二页的边界是不稳定的。因为MySQL执行LIMIT 20, 10时,如果排序键有重复,它的边界行选择顺序并不保证稳定。两次查询可能把同一行同时划入第一页和第二页,也可能某一行在两次分页中都被跳过。
解决方式:排序字段加一个唯一字段作为次级排序条件,推荐用主键:
var list = db.Queryable<Order>() .OrderBy(o => o.CreateTime, OrderByType.Desc) .OrderBy(o => o.Id, OrderByType.Desc) // 关键:补充唯一排序字段 .ToPageList(pageIndex, pageSize);这个习惯从我踩过坑之后就再也没丢掉。另外,MySQL对OFFSET过大的分页查询性能极差——你有一百万条记录,用户翻到第5万页,MySQL要扫描前一百万行再扔掉,耗时几十秒很正常。我实际项目里给大数据量的分页接口加了限制,单页最大100条,最大页码不超过总页数,否则直接返回空。
5.2 多表Update:SqlSugar没有封装好的API,别硬用
SqlSugar的Updateable<T>非常适用于单表更新,但多表联合更新它并没有像Queryable那样方便的API。我看到有同事尝试写:
db.Updateable<Order>() .SetColumns(o => new Order { Status = 1 }) .Where(o => o.UserId == 100) .ExecuteCommand();单表没问题,但当你需要UPDATE一个表的字段,却要根据另一个表关联条件来定位记录时,Updateable就力不从心了。
比如这个常见需求:把所有已经发货的订单的收货人姓名同步更新到用户主表的“最后收货人”字段:
UPDATE users u INNER JOIN orders o ON o.user_id = u.id SET u.last_receiver = o.receiver_name WHERE o.ship_status = 1;SqlSugar的Updateable没有直接支持这种Join Update的API,两种走法:
方案一,直接写原生SQL:
db.Ado.ExecuteCommand(@" UPDATE users u INNER JOIN orders o ON o.user_id = u.id SET u.last_receiver = o.receiver_name WHERE o.ship_status = 1; ");方案二,先在内存里查出受影响用户,再单表分批更新——但这种方法不推荐,数据量大时内存占满不说,还可能因为数据在两步操作之间变化而产生不一致。
我强烈建议直接用方案一。原生SQL只要参数化写好,不比ORM API的性能差。
5.3 原生SQL里的参数化,一个细节坑我很久
SqlSugar的Ado.SqlQuery支持参数化,写法:
var list = db.Ado.SqlQuery<Order>("SELECT * FROM orders WHERE status = @status AND create_time >= @start", new { status = 1, start = DateTime.UtcNow.AddDays(-7) });这里有个容易忽略的细节:MySQL连接器对参数名的处理。SqlSugar底层有两种连接器:MySql.Data和MySqlConnector。在MySql.Data下,参数名用@status没问题;但MySqlConnector下,如果用?status这种写法,某些版本会因为参数名被当作字符串变量而解析失败。我经历过一次:同样的代码,加装了一个NuGet包之后,所有带参数的原生SQL全部报Fatal error encountered during command execution,排查到最后发现是连接器从MySql.Data切换到了MySqlConnector,前缀差异导致的。
规避方式很简单:参数名统一用@前缀,并且new { }匿名对象里的属性名要和参数名完全一致,大小写不敏感但别拼错。多参数时,千万别用字符串拼接值,一定要用new { }传参,既防注入又省心。
5.4 参数数量限制,MySQL的隐形天花板
最后一个坑来自MySQL自身:单条SQL语句的参数数量上限,由max_allowed_packet决定,但实际开发中更容易撞到的是批量Insert时IN子句参数过多的问题。
比如你写一个查询:
var list = db.Queryable<Order>().In(o => o.Id, idList).ToList();如果idList有2万个元素,生成的SQL是WHERE id IN (@p1, @p2, ..., @p20000),这个SQL直接发出去,MySQL大概率报错Prepared statement contains too many placeholders。MySQL 8.0的占位符上限是65535个,2万个IN虽然不超,但配合批量Insert的场景,容易在一个不小心时冲破限制。
我的处理原则:IN查询一次不超过1000个参数,批量Insert一次不超过500个参数。超过这个量就分批执行。这不是性能最优原则,而是最不容易踩雷的保守策略。与其为了省几次网络请求硬冲上限,然后半夜被报警叫起来处理“线上批量任务失败”,不如老老实实分段执行。
6. 常见问题速查:把这几个症状和原因记在笔记里
我在日常答疑和被同事求助的过程中,整理了下面这份高频问题速查表,基本覆盖了C# + SqlSugar + MySQL最常见的翻车现场。
| 症状 | 可能原因 | 快速排查方法 |
|---|---|---|
Linux部署后报Table ... doesn't exist | 大小写命名策略不统一 | 执行SHOW VARIABLES LIKE 'lower_case_table_names',再看实体表名是否和库里一致 |
| 批量插入很慢 | 循环单条插入 | 换Insertable(list)或MySqlBulkCopy |
批量插入报PacketTooLargeException | 单批次数据量太大 | 调小batchSize,或检查max_allowed_packet |
| 时间差了8小时 | C#的DateTime.Now和数据库时区不一致 | 统一用DateTime.UtcNow,数据库落UTC |
| 读出来时间没有毫秒 | 列类型是datetime而非datetime(6) | 改列类型,实体加ColumnDataType指定 |
Convert.ToBoolean正常但(bool)强转抛异常 | MySQL底层返回了byte,非bool | 统一用Convert系列 |
| 聚合结果映射实体报转换异常 | MySQL返回类型是decimal/long,实体属性是int | SQL里显式CAST,或调整DTO类型 |
| 分页数据重复/漏数据 | 排序字段不唯一 | 排序条件加主键 |
| 原生SQL参数报错 | 连接器切换导致参数前缀不兼容 | 统一用@name,去掉?name写法 |
| IN条件太多报占位符溢出 | 单条SQL参数超上限 | 分批查询,单批不超过1000个参数 |
这个速查表的价值在于,它让你在线上故障时不至于靠肉眼一行行读日志。先看症状、再对原因,下一步才是改代码。
7. 新手特别容易踩的三个小坑
前面讲的都是“大坑”,最后补充三个细节上的“小坑”,也都来自真实项目。
7.1 连接字符串里的CharacterSet
MySQL 8.0默认字符集是utf8mb4,但如果你在连接字符串里不写CharacterSet=utf8mb4,而数据库表又是utf8mb4,SqlSugar查询出来的中文字符串可能在某些特殊字符(比如emoji)上出现乱码或者写入异常。这属于事前配置能避免的问题。
7.2 SqlSugar的IsAutoCloseConnection到底要不要开
建议开发环境设为true,让每次操作后自动关闭连接,避免连接泄漏;但生产环境如果并发很高,频繁开连接会增加开销。我这里没有标准答案,但我的实践是:生产环境设为false,配合using (var db = new SqlSugarClient(...))手动释放,业务代码里统一用using包裹。
7.3 CodeFirst更新表结构的风险
SqlSugar的CodeFirst在开发阶段很爽,实体加一个字段,启动时自动ALTER TABLE加列。但生产环境千万别默认开启这个功能,尤其是大型表,ALTER TABLE可能锁表几十分钟,业务直接全挂。我的项目里生产环境统一关闭CodeFirst的自动同步,表结构变更走脚本、走审查,上线前手动执行。
8. 排查问题的顺序也是一个经验
做了这么多SqlSugar相关项目,我总结出一个自己觉得效率最高的排查顺序,分享给你。
第一步,怀疑SqlSugar配置。出现任何莫名其妙的行为,先确认DbType是不是MySql、连接字符串有没有写错、实体特性有没有写对。很多人一上来就查SQL、查索引,结果发现是DbType=SqlServer没改过来,白白浪费半小时。
第二步,看生成的SQL。SqlSugar开启Aop.OnLogExecuting回调,把每次执行的SQL和参数打印出来,这个是最直观的排错手段。任何ORM概念上的疑问,到SQL这一层就清晰了。
db.Aop.OnLogExecuting = (sql, pars) => { Console.WriteLine($"SQL: {sql}"); foreach (var p in pars) { Console.WriteLine($"参数: {p.ParameterName} = {p.Value}"); } };第三步,直接拿生成的SQL到Navicat里手动执行。如果Navicat里也报错,那就是SQL或数据库本身的问题;如果Navicat里正常,那问题就在ORM和连接层。这个二分法能迅速缩小问题范围,省去大量无头苍蝇式的猜测。
第四步,看异常栈的底层异常类型。比如MySqlConnector.MySqlException和MySql.Data.MySqlClient.MySqlException命名空间不同,看异常类型就知道底层的驱动是哪一个,进而判断是否是驱动兼容性问题。
这个顺序我每次遇到问题都走一遍,大部分故障都能在20分钟内定位。说实话,SqlSugar本身不是一个完美的框架,它的文档也偶尔让人抓狂,但它的API设计和性能表现在中小型项目里确实是“够用且省心”的那一档。只要你把上面的这些坑提前避开,它完全可以稳定支撑生产环境。
如果你正好在做C# + MySQL的项目,建议把这篇文章里提到的小问题逐条自查一遍,尤其是命名策略、时区、批量插入这三项,十有八九能帮你省下一个通宵。