☰
MyBatis实战避坑指南:CRUD、动态SQL、参数映射与缓存优化
2026/10/17 13:00:19 网站建设 项目流程

干后端这些年,Mybatis 基本是绕不开的持久层框架。尤其现在 Spring Boot 项目里,十有六七的数据库访问层都是它。用的人多,踩的坑更多。我见过不少同事写 XML 还是老一套复制粘贴,动态 SQL 全靠 if 拼,参数传递靠猜,结果映射动不动报反射异常。其实这些问题的答案都在官网文档里,但文档太散,真要遇到事,还是得靠实战经验一条条攒。

这篇东西我不打算按官方文档的顺序讲,就按我在实际项目里最常用的操作场景来写。从基础 CRUD 讲到动态 SQL,再讲参数传递、结果映射,最后是缓存和常见报错排查。适合刚开始用 Mybatis 的初学者,也能给写了两三年但一直没系统整理过的老手查漏补缺。每一条都对应一个真实的业务场景,你可以直接拿去用。

1. 项目里最常用的几种基础操作模式

1.1 配置文件里那几个容易被忽略的开关

很多人刚接触 Mybatis 就是复制一套配置跑起来,从来不细看 settings 里到底有什么。其实几个关键开关直接影响项目行为,搞不清楚会在线上埋雷。

第一个是mapUnderscoreToCamelCase。数据库字段习惯用下划线命名(user_name),Java 属性用驼峰(userName)。这个开关设为 true 之后,Mybatis 会自动把下划线映射成驼峰,不用你手动写一堆 resultMap。我见过一些老项目没开这个,然后每个查询都写一个完整的 resultMap,几百行 XML 看着就头疼。加上这个配置,大部分查询的 resultType 直接用实体类就行。

第二个是jdbcTypeForNull。默认值是OTHER,在 Oracle 驱动下插入 null 值会报"无效的列类型"。很多团队在 MySQL 上没事,换到 Oracle 就炸,就是这个参数搞的。建议全局设成NULL,一劳永逸。

第三个是callSettersOnNulls。默认 false。查询结果里某列是 null 时,Mybatis 不会调用对应的 setter,导致返回对象里这个字段是 null 而不是你有默认值。如果你在 getter 里做过空值兜底,会失效。需要确保 null 也走 setter 时,把这个打开。

配置大概长这样:

<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="jdbcTypeForNull" value="NULL"/> <setting name="callSettersOnNulls" value="false"/> </settings>

还有个容易忽略的地方是mapper-locations的通配符。Spring Boot 项目里常见写法:

mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.example.demo.entity

**表示多级目录递归,*单级。如果你只写classpath:mapper/*.xml,那 mapper 包下的子目录就扫不到了。别问我怎么知道的,某个项目升级目录结构后数据库层全线 404,就是这个原因。

1.2 注解 SQL 和 XML 怎么选

Mybatis 支持用注解直接写 SQL,不用建 XML 文件。比如:

@Select("select * from user where id = #{id}") User findById(Long id);

简单场景下这个很香,代码都在一个文件里,看逻辑方便。但动态 SQL 一多,注解里的<script>标签写起来又丑又难维护。我的原则是:单表单查询、SQL 固定不变,用注解;多表关联、条件不定、需要动态拼接,用 XML。

另外一个细节:注解 SQL 里如果想写><这类比较符号,XML 里要转义(&lt;&gt;),注解里不用。这个很多人记反了,结果 XML 直接报错。

比如 XML 里写:

<select id="findAdult" resultType="User"> select * from user where age > 18 </select>

这里>可以不转义,但select * from user where age < 18里的<必须写成&lt;,否则解析器会认为它是标签开头。同理<=>=建议统一转义。最省事的办法是反过来把 SQL 里的小于号全写成语义安全的&lt;,养成习惯。

注解还有个容易踩的坑:分页时 count 查询。很多分页插件会先执行你写的 SQL 然后再包一层 count,但遇到group by或者 union 时 count 会算错。这种 SQL 就别用注解了,XML 里单独写一个 count 查询会更可控。

1.3 获取自增主键的两种姿势

插入数据后要拿到自增 ID,这个需求太常见了。用注解的话在插入方法上加点@Options:

@Insert("insert into user(name, age) values(#{name}, #{age})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(User user);

执行完user.getId()就能拿到自增 ID。XML 里对应的是:

<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> insert into user(name, age) values(#{name}, #{age}) </insert>

注意keyProperty对应的是 Java 对象的属性名,不是数据库列名。如果你传的是 Map,那 key 就是 Map 里的键名。很多人这里写错成keyColumn,那是数据库列名,只有在主键字段名和属性名不一致时才需要配合使用。简单场景一个keyProperty就够了。

如果你是批量插入然后要拿所有自增 ID,就得注意数据库驱动行为。MySQL 支持useGeneratedKeys对批量插入返回每个 ID,但要求insert语句是values多值形式(一条 insert 带多个 value 元组),并且 JDBC URL 里不要加rewriteBatchedStatements=false。如果用的是foreach拼接的批量插入,可以拿到每个对象的 ID。如果用的ExecutorType.BATCH又没开 rewrite,有些驱动版本可能只返回最后一个 ID,这个后面批量操作那一节细说。

2. 动态 SQL:用得最多也最容易翻车

2.1 四个核心标签的边界条件

Mybatis 动态 SQL 的四个主角是if、choose、where、set。逻辑都不难,但组合起来有很多细节。

if是最基础的,判断条件成立就拼接 SQL。最常见的错误是判断变量时只判断非空,不看是不是空字符串:

<if test="name != null and name != ''"> and name = #{name} </if>

数据库里某项不是必填,前端又可能传空串,这俩条件得一起写。还有一种情况是判断0值。比如状态字段status为0时也要作为查询条件,那test="status != null"就够了,不要加and status != '',因为 0 转字符串是"0",不等于空串,倒不会漏,但语义不清晰,容易让人误解。更常见的坑是有人写test="status != null and status != ''"把 0 放过去,实际没问题,但不必要。

choose相当于 Java 里的 switch:

<choose> <when test="type == 1"> and order_type = 'normal' </when> <when test="type == 2"> and order_type = 'vip' </when> <otherwise> and order_type = 'default' </otherwise> </choose>

注意when的条件是从前往后匹配第一个为 true 的,所以有优先级时要把最严格的条件放前面。比如"VIP 且金额大于100走 A 逻辑,其余 VIP 走 B 逻辑",那第一个 when 就得写test="vip and amount > 100"。反过来先判断vip的话,后面那个金额条件永远不会执行。

where标签主要用于去除多余的前缀AND。比如第一个条件不满足,第二个满足时,如果硬拼就是where and age = 18,直接 SQL 报错。where标签会自动去除语句开头的and或or。set标签同理,用于 update 语句里去除多余逗号。

这两个标签能处理 90% 的场景。但有个隐藏行为:where标签只在子元素有内容时才生成WHERE关键字。如果所有条件都不满足,最后生成的 SQL 就是select * from user不带 where。这在某些场景可能是隐患。比如你接口本来设计了分页,结果条件全空,全表查出来了。所以这种接口最好在代码层判断,条件为空时直接返回空 list,别把压力丢给数据库。

2.2 trim 的通用写法

很多人不知道where和set只是trim的特定形态。遇到更复杂的拼接需求,比如 update 语句中要去掉多余的AND和逗号,或者 insert 语句中处理动态列,直接用trim更灵活。

trim有四个属性:prefix(在结果前加前缀)、suffix(在结果后加后缀)、prefixOverrides(去掉结果前匹配的字符串)、suffixOverrides(去掉结果后匹配的字符串)。

where标签等价于:

<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>

set标签等价于:

<trim prefix="SET" suffixOverrides=","> ... </trim>

注意prefixOverrides里的字符串带空格是有意义的。有些博客写prefixOverrides="AND|OR"不带空格,实际匹配的是ANDxxx这种,而 SQL 拼接后的开头通常带空格,可能导致匹配不上。这个细节很多人忽略。

我之前做过一个动态更新接口,需求是"传入的字段才更新,且某几个字段必须同时更新",用set配合if写起来不直观,后来改成trim控制整个更新片段,代码可读性和正确性都高了不少。

2.3 foreach 不只是拼 in

foreach最常见的用法是拼in条件:

<select id="selectByIds" resultType="User"> select * from user where id in <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

collection属性有讲究:

  • 传入List,写list
  • 传入Set,写collection
  • 传入数组,写array
  • 传入 Map,写 Map 里的键名

但如果你在方法参数上加@Param("ids"),那collection直接写这个参数名即可。多个集合参数时,强烈建议加@Param,不然 Mybatis 的命名规则会让你写listcollectionarray混用,看着就想骂人。

foreach的另一个常用场景是批量插入:

<insert id="batchInsert"> insert into user(name, age, status) values <foreach collection="list" item="u" separator=","> (#{u.name}, #{u.age}, #{u.status}) </foreach> </insert>

这个写法在数据量几百上千时性能不错,因为只发起一次网络请求。但 MySQL 对单条 SQL 的长度有限制(默认max_allowed_packet通常是 64MB 或更小),一次性塞太多数据可能超出限制。稳妥的做法是分批插入,比如每 500 条一批切分,程序里分次调用 mapper 方法。

还有一种是批量更新。很多人循环里单条 update,几千条数据跑几分钟,慢得离谱。可以改成 case when:

<update id="batchUpdate"> update user set name = <foreach collection="list" item="u" separator=" " open="case id " close="end"> when #{u.id} then #{u.name} </foreach> where id in <foreach collection="list" item="u" separator="," open="(" close=")"> #{u.id} </foreach> </update>

这个 SQL 只执行一次。不过要注意:更新的字段是同一个name,如果每个对象更新的字段不同,这条写法就不适用了,需要另想办法,比如动态生成多条 update 再合并执行。

3. 参数传递与结果映射的细节

3.1 多个参数的几种传法

Mapper 接口里方法参数怎么传,直接决定 XML 里#{}怎么写。

单参数最简单。传实体类,#{属性名}直接用:

User findByNameAndAge(User user);
select * from user where name = #{name} and age = #{age}

传 Map 也一样,#{}里写键名。

多参数就要注意了。直接写两个参数,XML 里不写@Param也能用,但要用#{param1}#{param2}这种:

User findByNameAndAge(String name, Integer age);
select * from user where name = #{param1} and age = #{param2}

在老版本里写的是#{0}#{1},新版本改成了param1这种风格。不同版本的兼容性差异,是很多老项目升级后 SQL 突然报Parameter index out of range的原因之一。所以最稳妥的写法是明确加注解:

User findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
select * from user where name = #{name} and age = #{age}

这样读代码的人一眼知道哪个参数对应哪个字段,也摆脱了param1这种魔法数字。多参数 + 少量参数的场景强烈建议都用@Param。注意一个特例:如果你传的是一个实体类加一个其他参数,@Param其实可以不加在实体类上,但为了统一,我还是推荐加。

#{}和${}的区别这里必须说清楚。#{}是预编译参数占位符,Mybatis 会给它生成?,由 JDBC 驱动做参数绑定,天然防 SQL 注入。${}是字符串替换,直接拼进 SQL,不经过预编译,有注入风险。表名、排序字段不能用#{},因为占位符不能替代标识符,这类场景只能${},但必须自己做白名单校验。比如按列排序,只允许nameagecreate_time这几个固定值传进来,其他一律拒绝。

3.2 resultMap 的合理使用

简单查询直接用resultType足够,Mybatis 会按列名和属性名映射。开启了驼峰转换后,大部分列都能对上。但遇到以下两种场景就要上resultMap:

一种是列名对不上属性名,或者查询结果里有表达式列(比如count(*)或者sum(amount) as total_amount)。这种可以在 SQL 里直接起别名,配合驼峰转换也能搞定,没必要每次都建 resultMap。

另一种是多表联查结果里有嵌套对象。比如查订单带用户信息:

public class Order { private Long id; private String orderNo; private User user; }

SQL 联表查出来,希望自动把 user 相关的列填充进user属性。这时 resultMap 是必需品:

<resultMap id="OrderWithUserMap" type="Order"> <id property="id" column="id"/> <result property="orderNo" column="order_no"/> <association property="user" javaType="User"> <id property="id" column="user_id"/> <result property="name" column="user_name"/> </association> </resultMap>

一个常见的错误是嵌套对象里id没配<id>标签,只写了<result>。Mybatis 对<id>有性能优化,能加速相同主键的合并。不写的话,在某些关联查询里会出现重复数据结构,或者覆盖错数据。所以关联映射的id一定要显式标注。

3.3 嵌套查询与 N+1 问题

association和collection还有一种select属性写法,即子查询方式:

<resultMap id="OrderWithUserLazyMap" type="Order"> <association property="user" column="user_id" javaType="User" select="com.demo.mapper.UserMapper.selectById" fetchType="lazy"/> </resultMap>

查订单时,每个订单的user_id都会触发一次额外的用户查询。订单列表 100 条,就会多出 100 次查询,也就是经典的N+1 问题。这个必须警惕。

解决方案有两种:一种是上面说的联表查询,一次 SQL 全查出来;另一种是子查询,但配合fetchType="lazy"让它在实际访问属性时才加载,能有效减少无谓查询,但组合不当会造成懒加载异常(比如在事务外访问属性,报LazyInitializationException)。

我的习惯是:如果只是展示列表,数据量小且接口清晰,直接联表;如果关联对象本身很重,大概率用不到,可以用懒加载。但懒加载的正确性高度依赖会话状态,Spring 事务里一般没问题,一旦出了事务边界,容易翻车。新手不建议上来就懒加载,先把联表查询用明白。

4. 缓存、批处理与性能优化

4.1 一级缓存的生命周期和它的坑

Mybatis 默认开启一级缓存,作用范围是一个 SqlSession。同一个 SqlSession 里执行两次相同的查询 SQL,第二次会直接从缓存里取,不发 SQL 到数据库。

听起来很好,但实际项目里这个特性经常带来诡异问题。Spring 整合 Mybatis 后,SqlSession 的生命周期由框架管理,默认和事务绑定。在一个事务里执行两次相同查询,会命中一级缓存;但事务提交后 Session 关闭,缓存就清了。所以一级缓存对绝大多数业务流程是无感的。

真正的问题在于长事务里查询了数据,然后别的事务更新了这条记录,你的事务里再次查询,拿到的是旧数据。这个行为不是 Mybatis 的问题,而是数据库隔离级别 + 一级缓存叠加的效果。如果业务对实时性有要求,可以在配置里把一级缓存关了:

<setting name="localCacheScope" value="STATEMENT"/>

STATEMENT表示每次语句执行完就清空一级缓存。代价是同一个 SqlSession 里重复查询会被重复执行,但对一致性敏感的场景,这个取舍是值得的。

4.2 二级缓存:开启前必须想清楚的几件事

二级缓存是 namespace 级别的,也就是说同一个 Mapper XML 里的查询结果可以跨 SqlSession 共享。配置也比较简单:

  1. 在 XML 里加<cache/>标签
  2. 涉及缓存的实体类实现Serializable
  3. 查询和更新的 flushCache 策略默认行为通常够用

但二级缓存是个双刃剑。最大的风险在于:如果一个 Mapper 关联了多张表,而另一张表的数据被其他 Mapper 更新了,这个 Mapper 里的缓存并不知道,会继续返回脏数据。比如OrderMapper的查询缓存了订单和商品信息,ProductMapper更新了商品价格,OrderMapper 的缓存还是旧的。

网上很多教程把二级缓存吹得很神,我实际项目里基本不开。分布式环境下缓存一致性本就难保证,再加上 Mybatis 的二级缓存是进程内缓存,多实例部署时天然数据不一致,作用非常有限。真有缓存需求,直接用 Redis 做业务级缓存,可控性高得多。

如果一定要用,至少保证:一个二级缓存只服务于一张表,且这个表只有当前 Mapper 在操作。否则就该设flushInterval并理解缓存可能延迟生效。

4.3 ExecutorType.BATCH 和 rewriteBatchedStatements

Mybatis 支持三种 ExecutorType:SIMPLE(默认)、REUSE(预编译语句复用)、BATCH(批量执行)。批量写入场景下,BATCH能把多次单条 insert/update 攒成一批发送,减少网络往返。

开启方式:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = session.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } session.commit(); } finally { session.close(); }

注意几个细节:

  • BATCH模式下,update和delete也有批量效果,但查询不受影响,还是正常执行。
  • 如果是在 Spring 管理的 SqlSessionTemplate 里切换,需要配置ExecutorType。常见做法是给批处理单独建一个 SqlSessionTemplate,避免影响正常请求的线程。
  • BATCH 模式下如果有返回主键的需求,MySQL 配合useGeneratedKeys和rewriteBatchedStatements=true通常能拿到,但某些场景下可能必须一条条刷缓存(flushStatements)才能取到,这会打乱批次,性能回落。

还有一种纯 SQL 层面的优化——foreach拼接批量语句。这种方式对驱动无要求,兼容性最好,但单条 SQL 的包大小有限制。量小用 foreach,量大用 BATCH。两者对比:

方案网络请求次数单批上限主键返回兼容性
foreach 拼接1受 max_allowed_packet 限制支持各版本稳定
ExecutorType.BATCH多次但攒批发送受 batch 大小和驱动影响可能受限依赖驱动

具体业务里我会先算数据量,比如导 10 万条数据,foreach 一条条塞不可能,BATCH 模式每批 1000 条倒是比较稳定。

4.4 分页的插件选择与 count 优化

Mybatis 分页现在基本靠分页插件(比如某著名物理分页插件)。用法很简单,查列表前调用静态方法传入页码和每页条数,插件自动拼接 limit 并生成 count 查询。

但分页插件帮你生成 count 的方法不总是高效的。它默认把整条 SQL 包一层select count(*) from (原SQL) t,遇到多表 join 或者复杂条件时,这个 count 可能非常慢。这种情况下,可以在 mapper 里手动写一个更轻量的 count:

long countByCondition(@Param("condition") Query query);

插件也支持在分页时关闭自动 count 或者传入自定义 count 方法。

另一个注意点:PageHelper.startPage是ThreadLocal 实现,一旦调用后没有执行后续 mapper 查询(比如代码里做了异常抛出),ThreadLocal 里的分页参数不会被正确清掉,可能会影响到线程池里其他请求。务必在 finally 里调用PageHelper.clearPage()清理,或者严格保证 startPage 后立刻紧跟查询语句。

4.5 代码层循环查库的大坑

这个严格说不是 Mybatis 的问题,而是使用姿势问题,但非常典型。循环里调 mapper 查数据库,100 次循环就是 100 次网络往返,即使单条 SQL 只要 1ms,加上网络开销和连接获取,整体也会慢到不可接受。

改进方向:

  1. 循环外查一次批量接口,拿到结果后在内存里组装
  2. 用batch方式减少往返
  3. 把多次查询合并成一条in查询

实际性能对比我试过:同样 1000 条数据,循环单查大概耗时 800ms,批量 in 查询大概 15ms,差距两个数量级。这不是 Mybatis 慢,是数据库网络往返太贵,能少一次是一次。这个习惯比任何缓存优化都重要。

5. 常见报错排查与避坑实录

5.1 高频报错速查表

报错信息原因解决方案
Invalid bound statement (not found)mapper 接口方法在 XML 里没有对应 id,或 XML 没被扫描到检查 namespace、方法 id、mapper-locations 路径
Parameter index out of range (2 > number of parameters)SQL 里#{}参数和实际传入参数对不上用@Param明确命名参数
There is no getter for property named 'xxx'#{}里的属性名和传入对象对不上检查实体类属性名拼写
ResultMap collection query error嵌套 resultMap 的 collection 子查询配置错误检查column传参和selectid
Unknown column 'xxx' in 'field list'列名写错,或下划线列名没做驼峰映射检查 SQL 列名和数据库实际名
Bad SQL grammarSQL 拼接错误,多半是动态 SQL 产生语法错误打印完整 SQL,在数据库客户端执行验证
Only one result object expected查询返回多行但方法返回单对象检查业务数据唯一性,或改用返回 List
Keyset pagination is not supported分页插件和特殊 SQL 不兼容手动分页或简化 count 查询

第一行那个Invalid bound statement是我见过最多的,原因也最多样:XML 的 namespace 和接口全限定名不一致、方法名拼错、mapper-locations 没扫到子目录、还有的因为 XML 文件被构建工具过滤掉没打包到 classpath。排查顺序是:先看 target/classes 里有没有这个 XML,再看 namespace,最后看方法 id。

5.2 排查慢 SQL 和缓存 SQL 的方法

遇到 Mybatis 相关性能问题,第一步不是看代码,而是先拿到 SQL。怎么拿?两种方式。

一种是配置完整 SQL 日志打印:

logging: level: com.example.demo.mapper: debug

Mybatis 会打印Preparing:和Parameters:两行,可以在数据库客户端复制 SQL 手动执行验证。但注意:Preparing行显示的是?占位符,不是完整 SQL,需要把参数一个个手动替换进去验证。想直接看拼好的完整 SQL,需要引入第三方 SQL 日志插件或者自己在数据库层面开启慢查询日志。

另一种是开启 MySQL 的 slow_query_log:

set global slow_query_log = 'ON'; set global long_query_time = 1;

然后去日志目录看慢查询记录。这种能直接看到真实执行的完整 SQL 和耗时,定位效率很高。如果发现具体某条 SQL 慢,再用EXPLAIN看执行计划,检查索引是否命中。

常见场景:某张表数据量涨到百万级之前一切正常,之后接口突然慢了几秒。EXPLAIN一看type=ALL,全表扫描。这种多半是查询条件里对列做了函数操作导致索引失效,比如WHERE DATE(create_time) = CURDATE(),或者LIKE '%xxx%'前置通配符导致索引失效。前者改成范围查询create_time >= ? and create_time < ?,后者考虑全文索引或搜索引擎方案。

5.3 动态拼接 SQL 时常见的隐蔽错误

第一个是<if>里的字符串比较。比如判断status == '1'时,单引号包着的字符串在 Mybatis 的 OGNL 表达式里其实是字符串字面量的 char 类型,在某些版本比较时会出问题。稳妥做法是status == '1'.toString()或者"1".equals(status)。我见过一个项目里全用== '1',升级 Mybatis 版本后部分判断突然失效,排查了很久。

第二个是foreach里separator=","和open="("的组合容易出格式问题。比如in后面漏了空格,拼出in(1,2)没问题,但拼出in(1,2)少个空格,SQL 也能跑,可一旦open前面忘了加空格,拼出来的就变成idin这种语法错误。建议在动态 SQL 每个片段前后都习惯性留空格,避免拼接出非法 SQL。

第三个是 XML 里用了保留字当列名或表名。比如列名desc、order、group,不转义直接写,轻则 SQL 报错,重则被当成 SQL 关键字产生预料之外的行为。MySQL 里可以用反引号包起来,比如`desc`。Oracle 和 SQL Server 的转义不一样,跨数据库项目要特别注意。

5.4 实际开发中几个容易被忽视的小问题

5.4.1 时间类型的时区问题

数据库存的是datetime,Java 端用LocalDateTime接收,看起来没问题。但如果你传输给前端时转成Date或者字符串,时区没统一,前端看到的可能差 8 小时。问题源头可能根本不在 Mybatis,而是 JDBC URL 里的serverTimezone参数。MySQL 8 连接串建议写成:

jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

allowPublicKeyRetrieval=true是 MySQL 8 客户端连接时需要的,很多老同事从 MySQL 5.7 迁移过来卡在这一步。如果项目里所有时间都差 8 小时,多半就是时区配置问题;如果只有个别接口差,才可能是字段映射的格式化问题。

5.4.2 大数据量导出时的逐行查询

分页列表接口没问题,但导出报表这种全量查询场景,千万别一次性把所有数据加载到内存。正确姿势是用游标读取,Mybatis 的Cursor可以逐条加载:

@Select("select * from big_table") Cursor<BigTable> selectAll();

用游标时要注意:必须保持 SqlSession 打开,读完再关;游标期间不要做其他会关闭 Session 的操作;处理完全部数据后记得 close。这个方案比一次性 List 更省内存,也不容易 OOM。但游标是只前向的,不能在遍历一半时做再次查询同一个数据库对象的操作,否则可能导致连接占用问题。能用分批查询解决的,优先分批。

5.4.3 逻辑删除和唯一索引的冲突

很多项目用逻辑删除,deleted字段标记是否删除。但deleted只有 0 和 1 两个值,唯一索引比如uk_name时,每次删除后想重新插入同名的记录,会撞唯一索引。

常见解法有几个:

  1. 把deleted改成时间戳(deleted_at),存在时存时间,没删除存 0 或 null。这样每条删除记录都有不同的deleted_at值,可以参与唯一索引。
  2. 唯一索引把deleted字段一起加进去,但只能解决首次删除的冲突,第二次删除还会撞。
  3. 删除时生成随机串写到deleted_at,让每次删除都各不相同。

这个坑在数据量小的时候不会暴露,等业务逻辑围绕唯一性约束写死后,再改就难受了。建表时就要想清楚删除策略和索引约束的关系。

最后分享两个实际操作经验

一个是团队协作时,Mybatis XML 里的 SQL 格式最好统一。IDEA 里装个格式化插件,提交前把 XML 里的缩进和换行统一一下,review 时能少很多无意义的 diff。另一个是如果接口量多,建议做一层通用的 BaseMapper,把单表的增删改查、分页、批量操作全封装进去,具体 Mapper 只写特殊查询。这样既避免每个 Mapper 重复造轮子,也方便在通用层统一加字段(比如创建时间、更新时间自动填充)。

Mybatis 的核心其实还是 SQL,框架只是帮你省去了 JDBC 的模板代码。真正把 SQL 写好、把动态条件控制好、把结果映射设计好,不管是 Mybatis 还是别的 ORM,落到数据库层面的能力才是根本。这也就是为什么我更愿意把这套操作经验沉淀成文档而不是指望框架自动解决一切。希望这篇东西能帮你少踩几个坑。

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

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

立即咨询