2026/4/17 9:33:05
网站建设
项目流程
wordpress360网站卫士,网站备案查询流程,网店货源,稳定免费空间1. 理解saveOrUpdate的核心机制
MybatisPlus的saveOrUpdate方法是一个让人又爱又恨的功能。它表面上看起来很简单——根据主键是否存在来决定是插入还是更新数据。但实际使用中#xff0c;我发现这个方法的坑远比想象中要多。
先说说它的基本工作原理。当你不带任何条件构造器…1. 理解saveOrUpdate的核心机制MybatisPlus的saveOrUpdate方法是一个让人又爱又恨的功能。它表面上看起来很简单——根据主键是否存在来决定是插入还是更新数据。但实际使用中我发现这个方法的坑远比想象中要多。先说说它的基本工作原理。当你不带任何条件构造器调用saveOrUpdate时MybatisPlus会先根据实体类的主键去数据库查询。如果查到了记录就执行更新操作如果没查到就执行插入操作。听起来很合理对吧但问题就出在这个主键的判断上。我遇到过最典型的问题就是自动递增主键的场景。假设我们有个用户表主键是自增ID。当我新建一个用户对象不设置ID值直接调用saveOrUpdate时会发生什么按照常理这应该是个新用户应该执行插入操作。但MybatisPlus会先执行一个查询SELECT * FROM user WHERE idnull。显然这个查询会返回空于是它就会执行插入。看起来没问题但如果你传入了UpdateWrapper情况就变得复杂了。2. 非主键字段冲突的常见场景在实际开发中我们经常需要根据业务唯一键而不是主键来判断记录是否存在。比如用户表除了自增主键ID外还有手机号字段需要保持唯一。这种情况下saveOrUpdate的默认行为就完全不能满足需求了。我最近就遇到了这样一个案例一个电商系统的商品分类表。分类有自增ID作为主键但同时分类名称也必须是唯一的。当用户提交一个分类数据时我们需要判断如果分类名称已存在就更新该分类如果不存在就新建分类。按照saveOrUpdate的默认逻辑它会根据主键ID来判断而我们的ID是自增的永远都是新ID这就导致每次都会执行插入操作最终造成分类名称重复的数据。这显然不是我们想要的结果。3. 使用UpdateWrapper解决非主键冲突经过一番折腾我发现可以通过UpdateWrapper来解决这个问题。具体做法是这样的UpdateWrapperCategory wrapper new UpdateWrapperCategory() .eq(category_name, category.getCategoryName()); categoryService.saveOrUpdate(category, wrapper);这段代码的逻辑是先尝试根据分类名称更新记录如果更新影响的行数为0说明没有这个分类名称的记录再执行默认的saveOrUpdate逻辑。但这里有个细节需要注意UpdateWrapper的eq条件字段必须是数据库列名而不是实体类属性名。如果你不小心用了驼峰命名比如categoryName就会报错。这个坑我踩过好几次现在都会特别注意。另外UpdateWrapper和QueryWrapper在这个场景下的表现是不同的。虽然方法签名接受的是Wrapper类型但如果你传入QueryWrapperMybatisPlus会把它当作UpdateWrapper来处理。这点从源码中也能看出来default boolean saveOrUpdate(T entity, WrapperT updateWrapper) { return this.update(entity, updateWrapper) || this.saveOrUpdate(entity); }4. ON DUPLICATE KEY UPDATE的优化方案对于MySQL数据库其实有更高效的解决方案——ON DUPLICATE KEY UPDATE语法。这是MySQL特有的语法可以在一次SQL操作中完成存在则更新不存在则插入的逻辑。假设我们的分类表结构如下CREATE TABLE category ( id BIGINT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(100) UNIQUE, description VARCHAR(500), create_time DATETIME );我们可以这样使用ON DUPLICATE KEY UPDATEINSERT INTO category (category_name, description, create_time) VALUES (电子产品, 各类电子设备, NOW()) ON DUPLICATE KEY UPDATE description VALUES(description), create_time VALUES(create_time);这条SQL的意思是尝试插入一条新记录如果category_name因为有UNIQUE约束已存在就更新description和create_time字段。在MybatisPlus中我们可以通过自定义SQL的方式使用这个特性。首先在Mapper接口中定义方法Insert(INSERT INTO category (category_name, description, create_time) VALUES (#{categoryName}, #{description}, NOW()) ON DUPLICATE KEY UPDATE description VALUES(description), create_time VALUES(create_time)) int saveOrUpdateByCategoryName(Category category);这种方式的性能明显优于先查询再判断的方案因为它只需要一次数据库交互。特别是在批量操作时优势更加明显。5. 批量操作的性能优化说到批量操作MybatisPlus自带的saveOrUpdateBatch方法在处理非主键冲突时也有局限性。它本质上还是循环调用单条的saveOrUpdate性能并不理想。对于大批量数据我们可以利用MySQL的ON DUPLICATE KEY UPDATE特性来实现真正的批量操作。下面是一个示例Insert(script INSERT INTO category (category_name, description, create_time) VALUES foreach collectionlist itemitem separator, (#{item.categoryName}, #{item.description}, NOW()) /foreach ON DUPLICATE KEY UPDATE description VALUES(description), create_time VALUES(create_time) /script) int batchSaveOrUpdate(Param(list) ListCategory categories);这个方案在处理上千条数据时性能可以提升几十倍。我在实际项目中做过测试插入1000条数据其中约30%需要更新使用saveOrUpdateBatch需要约5秒而这个批量方案只需要不到0.1秒。6. 主键回填的注意事项使用saveOrUpdate时另一个需要注意的问题是主键回填。对于插入操作我们通常需要获取数据库生成的主键值。Mybatis本身支持主键回填但在saveOrUpdate场景下情况会复杂一些。如果使用默认的saveOrUpdate方法当执行插入操作时实体对象的主键字段会被自动填充。但如果是通过UpdateWrapper先执行更新操作就不会有主键回填因为更新的记录主键本来就是已知的。这里有个容易混淆的点即使更新操作影响了多行记录MybatisPlus也只会把Wrapper中eq条件对应的主键值回填到实体对象。如果你需要获取所有被更新记录的主键就需要额外查询。7. 事务与并发控制在高并发场景下使用saveOrUpdate还需要考虑事务和并发问题。特别是当多个线程同时判断记录不存在并尝试插入时可能会引发唯一键冲突。我建议在这些场景下确保数据库相关字段有正确的唯一索引使用事务保证操作的原子性考虑添加适当的重试机制Spring的Transactional注解可以很方便地管理事务Transactional public void saveOrUpdateCategory(Category category) { UpdateWrapperCategory wrapper new UpdateWrapperCategory() .eq(category_name, category.getCategoryName()); categoryService.saveOrUpdate(category, wrapper); }8. 实际项目中的最佳实践经过多个项目的实践我总结出以下几点经验对于简单的单条操作可以使用saveOrUpdate加UpdateWrapper对于批量操作优先考虑ON DUPLICATE KEY UPDATE方案确保所有业务唯一键都有数据库层面的唯一索引约束在高并发场景下配合使用事务和重试机制注意监控和日志及时发现和处理冲突情况一个完整的工具类示例public class CategoryService { Autowired private CategoryMapper categoryMapper; // 单条保存或更新 Transactional public void saveOrUpdateByName(Category category) { UpdateWrapperCategory wrapper new UpdateWrapperCategory() .eq(category_name, category.getCategoryName()); if (!categoryService.saveOrUpdate(category, wrapper)) { throw new RuntimeException(保存分类失败); } } // 批量保存或更新 Transactional public void batchSaveOrUpdate(ListCategory categories) { if (CollectionUtils.isEmpty(categories)) { return; } int affected categoryMapper.batchSaveOrUpdate(categories); log.info(批量保存分类处理{}条影响{}行, categories.size(), affected); } // 带重试的保存 Transactional public void saveWithRetry(Category category, int maxRetries) { int retries 0; while (retries maxRetries) { try { saveOrUpdateByName(category); return; } catch (Exception e) { retries; if (retries maxRetries) { throw e; } try { Thread.sleep(100 * retries); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException(ie); } } } } }9. 性能对比与选型建议为了帮助大家更好地选择方案我做了个简单的性能对比方案单条耗时(ms)1000条耗时(ms)适用场景saveOrUpdate默认15-2015000-20000简单场景数据量小saveOrUpdateWrapper20-2520000-25000非主键判断数据量小ON DUPLICATE单条5-105000-10000MySQL单条或小批量ON DUPLICATE批量-50-100MySQL大批量操作从对比可以看出对于大批量操作ON DUPLICATE KEY UPDATE方案具有绝对优势。但在非MySQL数据库或需要兼容多数据库的场景下还是得使用saveOrUpdate加Wrapper的方案。10. 常见问题排查在使用过程中可能会遇到各种问题。这里分享几个常见问题的排查方法报错can not find column for id from entity检查实体类是否使用了TableId注解标记主键字段根据非主键字段更新不生效确认UpdateWrapper的条件字段名是数据库列名而非实体属性名 检查数据库是否有对应的唯一索引批量操作性能差考虑使用ON DUPLICATE KEY UPDATE批量方案 检查数据库连接池配置是否合理主键没有回填确认操作实际执行的是插入而非更新 检查实体类主键字段的TableId配置高并发下出现重复数据确保数据库有唯一索引约束 考虑添加分布式锁或重试机制11. 源码解析与扩展思路对于想深入理解saveOrUpdate原理的开发者可以看看MybatisPlus的源码。核心逻辑在com.baomidou.mybatisplus.extension.service.IService中default boolean saveOrUpdate(T entity) { if (null ! entity) { Class? cls entity.getClass(); TableInfo tableInfo TableInfoHelper.getTableInfo(cls); if (null ! tableInfo tableInfo.isWithInsertFill() null tableInfo.getKeyProperty()) { return save(entity); } } return null getById(entity) ? save(entity) : updateById(entity); }这段代码清晰地展示了默认的saveOrUpdate逻辑先尝试根据ID查询再决定插入或更新。如果你想实现更复杂的逻辑比如根据多个字段组合判断记录是否存在可以考虑继承ServiceImpl类并重写相关方法。这种扩展方式既能保持MybatisPlus的便利性又能满足特定业务需求。