一、insertOrUpdate功能概述 (一)什么是insertOrUpdate insertOrUpdate是MyBatis Plus提供的一个非常实用的功能,它能够智能地判断数据是否存在:
如果数据不存在 :执行INSERT操作
如果数据已存在 :执行UPDATE操作
这个功能在实际开发中非常常见,特别是在数据同步、批量处理等场景中。
(二)使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Service public class UserService extends ServiceImpl <UserMapper, User> { public boolean saveOrUpdateUser (User user) { return this .saveOrUpdate(user); } public boolean batchSaveOrUpdate (List<User> userList) { return this .saveOrUpdateBatch(userList); } } @RestController public class UserController { @Autowired private UserService userService; @PostMapping("/user/saveOrUpdate") public Result saveOrUpdateUser (@RequestBody User user) { boolean success = userService.saveOrUpdate(user); return success ? Result.success() : Result.error(); } }
(三)核心方法介绍 MyBatis Plus提供了多个insertOrUpdate相关的方法:
方法名
描述
适用场景
saveOrUpdate(T entity)
单个实体的保存或更新
单条数据处理
saveOrUpdateBatch(Collection<T> entityList)
批量保存或更新
批量数据处理
saveOrUpdateBatch(Collection<T> entityList, int batchSize)
指定批次大小的批量操作
大数据量处理
二、实现原理深度分析 (一)判断逻辑:如何确定是INSERT还是UPDATE 1. 主键判断策略 MyBatis Plus的insertOrUpdate主要通过以下逻辑来判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean saveOrUpdate (T entity) { Object idVal = getIdVal(entity); if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) { return save(entity); } else { return updateById(entity); } }
2. 详细判断流程 flowchart TD
A[开始 saveOrUpdate] --> B{主键值是否为空?}
B -->|是| C[执行 INSERT 操作]
B -->|否| D{数据库中是否存在该记录?}
D -->|不存在| C
D -->|存在| E[执行 UPDATE 操作]
C --> F[返回操作结果]
E --> F
style A fill:#e1f5fe
style C fill:#c8e6c9
style E fill:#fff3e0
style F fill:#f3e5f5
(二)源码分析 1. ServiceImpl中的saveOrUpdate实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public boolean saveOrUpdate (T entity) { if (null != entity) { TableInfo tableInfo = TableInfoHelper.getTableInfo(this .entityClass); Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!" ); String keyProperty = tableInfo.getKeyProperty(); Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!" ); Object idVal = tableInfo.getPropertyValue(entity, tableInfo.getKeyProperty()); return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) : updateById(entity); } return false ; }
2. 主键值获取逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Object getPropertyValue (Object entity, String property) { if (null == entity || StringUtils.isBlank(property)) { return null ; } try { String getterName = "get" + StringUtils.capitalize(property); Method getter = entity.getClass().getMethod(getterName); return getter.invoke(entity); } catch (Exception e) { try { Field field = entity.getClass().getDeclaredField(property); field.setAccessible(true ); return field.get(entity); } catch (Exception ex) { throw new RuntimeException ("无法获取属性值: " + property, ex); } } }
3. 批量操作实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override @Transactional(rollbackFor = Exception.class) public boolean saveOrUpdateBatch (Collection<T> entityList, int batchSize) { TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!" ); String keyProperty = tableInfo.getKeyProperty(); Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!" ); List<T> insertList = new ArrayList <>(); List<T> updateList = new ArrayList <>(); for (T entity : entityList) { Object idVal = tableInfo.getPropertyValue(entity, keyProperty); if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) { insertList.add(entity); } else { updateList.add(entity); } } boolean insertResult = insertList.isEmpty() || saveBatch(insertList, batchSize); boolean updateResult = updateList.isEmpty() || updateBatchById(updateList, batchSize); return insertResult && updateResult; }
(三)关键组件分析 1. TableInfo表信息缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class TableInfo { private String tableName; private TableId tableId; private String keyProperty; private String keyColumn; private List<TableField> fieldList; public Object getPropertyValue (Object entity, String property) { return ReflectionKit.getFieldValue(entity, property); } } public class TableInfoHelper { private static final Map<Class<?>, TableInfo> TABLE_INFO_CACHE = new ConcurrentHashMap <>(); public static TableInfo getTableInfo (Class<?> clazz) { return TABLE_INFO_CACHE.get(clazz); } }
2. 主键策略处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public enum IdType { AUTO(0 ), NONE(1 ), INPUT(2 ), ASSIGN_ID(3 ), ASSIGN_UUID(4 ); } public boolean saveOrUpdate (T entity) { TableInfo tableInfo = TableInfoHelper.getTableInfo(this .entityClass); String keyProperty = tableInfo.getKeyProperty(); Object idVal = tableInfo.getPropertyValue(entity, keyProperty); if (tableInfo.getIdType() == IdType.AUTO) { return (idVal == null || (idVal instanceof Number && ((Number) idVal).longValue() == 0 )) ? save(entity) : updateById(entity); } else { return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) : updateById(entity); } }
三、性能分析与优化 (一)性能问题分析 1. 数据库查询开销 1 2 3 4 5 6 7 8 9 10 11 public boolean saveOrUpdate (T entity) { Object idVal = getIdVal(entity); if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) { return save(entity); } else { return updateById(entity); } }
2. 批量操作的性能瓶颈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean saveOrUpdateBatch (Collection<T> entityList, int batchSize) { for (T entity : entityList) { Object idVal = getIdVal(entity); if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) { insertList.add(entity); } else { updateList.add(entity); } } }
(二)性能优化方案 1. 使用MySQL的ON DUPLICATE KEY UPDATE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Mapper public interface UserMapper extends BaseMapper <User> { @Insert("INSERT INTO user (id, name, email, age) VALUES (#{id}, #{name}, #{email}, #{age}) " + "ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email), age = VALUES(age)") int insertOrUpdate (User user) ; @Insert("<script>" + "INSERT INTO user (id, name, email, age) VALUES " + "<foreach collection='list' item='item' separator=','>" + "(#{item.id}, #{item.name}, #{item.email}, #{item.age})" + "</foreach>" + "ON DUPLICATE KEY UPDATE " + "name = VALUES(name), email = VALUES(email), age = VALUES(age)" + "</script>") int batchInsertOrUpdate (@Param("list") List<User> userList) ; }
2. 批量查询优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Service public class OptimizedUserService extends ServiceImpl <UserMapper, User> { public boolean optimizedSaveOrUpdateBatch (List<User> userList) { if (CollectionUtils.isEmpty(userList)) { return true ; } List<Long> idList = userList.stream() .map(User::getId) .filter(Objects::nonNull) .collect(Collectors.toList()); Set<Long> existingIds = new HashSet <>(); if (!idList.isEmpty()) { List<User> existingUsers = this .listByIds(idList); existingIds = existingUsers.stream() .map(User::getId) .collect(Collectors.toSet()); } List<User> insertList = new ArrayList <>(); List<User> updateList = new ArrayList <>(); for (User user : userList) { if (user.getId() == null || !existingIds.contains(user.getId())) { insertList.add(user); } else { updateList.add(user); } } boolean insertResult = insertList.isEmpty() || this .saveBatch(insertList); boolean updateResult = updateList.isEmpty() || this .updateBatchById(updateList); return insertResult && updateResult; } }
3. 使用缓存优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Service public class CachedUserService extends ServiceImpl <UserMapper, User> { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String USER_EXISTS_KEY = "user:exists:" ; private static final int CACHE_EXPIRE_SECONDS = 300 ; public boolean cachedSaveOrUpdate (User user) { if (user.getId() == null ) { return this .save(user); } String cacheKey = USER_EXISTS_KEY + user.getId(); Boolean exists = (Boolean) redisTemplate.opsForValue().get(cacheKey); if (exists == null ) { User existingUser = this .getById(user.getId()); exists = existingUser != null ; redisTemplate.opsForValue().set(cacheKey, exists, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS); } if (exists) { boolean result = this .updateById(user); if (result) { redisTemplate.opsForValue().set(cacheKey, true , CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS); } return result; } else { boolean result = this .save(user); if (result) { redisTemplate.opsForValue().set(cacheKey, true , CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS); } return result; } } }
四、实际应用场景与最佳实践 (一)常见应用场景 1. 数据同步场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Service public class UserSyncService { @Autowired private UserService userService; public void syncUsersFromExternalSystem () { List<ExternalUser> externalUsers = externalSystemClient.getUsers(); List<User> users = externalUsers.stream() .map(this ::convertToInternalUser) .collect(Collectors.toList()); userService.saveOrUpdateBatch(users); log.info("同步用户数据完成,共处理 {} 条记录" , users.size()); } private User convertToInternalUser (ExternalUser externalUser) { User user = new User (); user.setId(externalUser.getExternalId()); user.setName(externalUser.getName()); user.setEmail(externalUser.getEmail()); user.setUpdateTime(new Date ()); return user; } }
2. 配置管理场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Service public class ConfigService extends ServiceImpl <ConfigMapper, SystemConfig> { public void updateConfigs (Map<String, String> configMap) { List<SystemConfig> configs = configMap.entrySet().stream() .map(entry -> { SystemConfig config = new SystemConfig (); config.setConfigKey(entry.getKey()); config.setConfigValue(entry.getValue()); config.setUpdateTime(new Date ()); return config; }) .collect(Collectors.toList()); this .saveOrUpdateBatch(configs); } public void updateConfig (String key, String value) { SystemConfig config = new SystemConfig (); config.setConfigKey(key); config.setConfigValue(value); config.setUpdateTime(new Date ()); this .saveOrUpdate(config); } }
3. 缓存预热场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Service public class CacheWarmupService { @Autowired private ProductService productService; @Autowired private RedisTemplate<String, Object> redisTemplate; @Scheduled(cron = "0 0 2 * * ?") public void warmupProductCache () { List<Product> products = getProductsForWarmup(); productService.saveOrUpdateBatch(products); products.forEach(product -> { String cacheKey = "product:" + product.getId(); redisTemplate.opsForValue().set(cacheKey, product, 1 , TimeUnit.HOURS); }); log.info("商品缓存预热完成,处理商品数量:{}" , products.size()); } }
(二)最佳实践 1. 主键策略选择 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Entity @TableName("user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private String email; @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; }
2. 批量操作优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Service public class OptimizedBatchService { private static final int DEFAULT_BATCH_SIZE = 1000 ; public void processBigDataList (List<User> userList) { if (CollectionUtils.isEmpty(userList)) { return ; } List<List<User>> batches = Lists.partition(userList, DEFAULT_BATCH_SIZE); for (List<User> batch : batches) { try { processOneBatch(batch); } catch (Exception e) { log.error("批次处理失败,批次大小:{}" , batch.size(), e); } } } @Transactional(rollbackFor = Exception.class) public void processOneBatch (List<User> batch) { userService.saveOrUpdateBatch(batch, DEFAULT_BATCH_SIZE); } }
3. 异常处理与监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 @Service public class MonitoredUserService extends ServiceImpl <UserMapper, User> { private static final Logger log = LoggerFactory.getLogger(MonitoredUserService.class); @Override public boolean saveOrUpdate (T entity) { long startTime = System.currentTimeMillis(); String operation = "saveOrUpdate" ; try { if (entity == null ) { throw new IllegalArgumentException ("实体对象不能为空" ); } boolean result = super .saveOrUpdate(entity); long duration = System.currentTimeMillis() - startTime; log.info("{}操作成功,耗时:{}ms,实体:{}" , operation, duration, entity.getClass().getSimpleName()); return result; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; log.error("{}操作失败,耗时:{}ms,实体:{},错误:{}" , operation, duration, entity.getClass().getSimpleName(), e.getMessage(), e); throw new RuntimeException ("saveOrUpdate操作失败" , e); } } @Override public boolean saveOrUpdateBatch (Collection<T> entityList, int batchSize) { if (CollectionUtils.isEmpty(entityList)) { return true ; } long startTime = System.currentTimeMillis(); String operation = "saveOrUpdateBatch" ; try { boolean result = super .saveOrUpdateBatch(entityList, batchSize); long duration = System.currentTimeMillis() - startTime; log.info("{}操作成功,耗时:{}ms,处理数量:{},批次大小:{}" , operation, duration, entityList.size(), batchSize); return result; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; log.error("{}操作失败,耗时:{}ms,处理数量:{},批次大小:{},错误:{}" , operation, duration, entityList.size(), batchSize, e.getMessage(), e); throw new RuntimeException ("saveOrUpdateBatch操作失败" , e); } } }
4. 配置优化建议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 mybatis-plus: configuration: map-underscore-to-camel-case: true cache-enabled: true default-statement-timeout: 30 global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 insert-strategy: not_null update-strategy: not_null spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000
五、常见问题与解决方案 (一)常见问题 1. 主键为null时的处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testSaveOrUpdateWithNullId () { User user = new User (); user.setId(null ); user.setName("张三" ); user.setEmail("zhangsan@example.com" ); boolean result = userService.saveOrUpdate(user); System.out.println("插入后的ID:" + user.getId()); }
2. 复合主键的处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Entity @TableName("user_role") public class UserRole { @TableId(type = IdType.INPUT) private Long userId; @TableId(type = IdType.INPUT) private Long roleId; private Date createTime; @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; UserRole userRole = (UserRole) o; return Objects.equals(userId, userRole.userId) && Objects.equals(roleId, userRole.roleId); } @Override public int hashCode () { return Objects.hash(userId, roleId); } } @Service public class UserRoleService extends ServiceImpl <UserRoleMapper, UserRole> { @Override public boolean saveOrUpdate (UserRole entity) { if (entity.getUserId() == null || entity.getRoleId() == null ) { return this .save(entity); } QueryWrapper<UserRole> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("user_id" , entity.getUserId()) .eq("role_id" , entity.getRoleId()); UserRole existing = this .getOne(queryWrapper); if (existing == null ) { return this .save(entity); } else { return this .update(entity, queryWrapper); } } }
3. 并发问题处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Service public class ConcurrentSafeUserService extends ServiceImpl <UserMapper, User> { @Override public boolean saveOrUpdate (User entity) { if (entity.getId() == null ) { return this .save(entity); } User existing = this .getById(entity.getId()); if (existing == null ) { return this .save(entity); } else { entity.setVersion(existing.getVersion()); return this .updateById(entity); } } @Autowired private RedisTemplate<String, Object> redisTemplate; public boolean saveOrUpdateWithLock (User entity) { String lockKey = "user:lock:" + entity.getId(); String lockValue = UUID.randomUUID().toString(); try { Boolean lockAcquired = redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 10 , TimeUnit.SECONDS); if (!lockAcquired) { throw new RuntimeException ("获取锁失败,请稍后重试" ); } return this .saveOrUpdate(entity); } finally { String currentValue = (String) redisTemplate.opsForValue().get(lockKey); if (lockValue.equals(currentValue)) { redisTemplate.delete(lockKey); } } } }
(二)性能调优建议 1. SQL执行计划分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 EXPLAIN SELECT * FROM user WHERE id = 1 ; EXPLAIN INSERT INTO user (id, name, email) VALUES (1 , '张三' , 'zhangsan@example.com' ); EXPLAIN UPDATE user SET name = '李四' WHERE id = 1 ; EXPLAIN INSERT INTO user (id, name, email) VALUES (1 , '张三' , 'zhangsan@example.com' ) ON DUPLICATE KEY UPDATE name = VALUES (name), email = VALUES (email);
2. 索引优化建议 1 2 3 4 5 6 7 8 9 10 11 12 13 ALTER TABLE user ADD PRIMARY KEY (id);ALTER TABLE user ADD UNIQUE INDEX uk_email (email);ALTER TABLE user ADD INDEX idx_name_email (name, email);ALTER TABLE user ADD INDEX idx_covering (id, name, email, create_time);
3. 监控指标 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class SaveOrUpdateMetrics { private final MeterRegistry meterRegistry; private final Counter saveCounter; private final Counter updateCounter; private final Timer saveOrUpdateTimer; public SaveOrUpdateMetrics (MeterRegistry meterRegistry) { this .meterRegistry = meterRegistry; this .saveCounter = Counter.builder("saveOrUpdate.save.count" ) .description("保存操作计数" ) .register(meterRegistry); this .updateCounter = Counter.builder("saveOrUpdate.update.count" ) .description("更新操作计数" ) .register(meterRegistry); this .saveOrUpdateTimer = Timer.builder("saveOrUpdate.duration" ) .description("saveOrUpdate操作耗时" ) .register(meterRegistry); } public void recordSave () { saveCounter.increment(); } public void recordUpdate () { updateCounter.increment(); } public Timer.Sample startTimer () { return Timer.start(meterRegistry); } }
六、总结与建议 (一)核心要点总结
实现原理 :
MyBatis Plus的saveOrUpdate通过主键值判断执行INSERT或UPDATE
核心逻辑是先检查主键是否为空,再查询数据库确认记录是否存在
批量操作会分离数据为插入列表和更新列表,分别执行
性能考虑 :
每次操作都会执行额外的SELECT查询,在大数据量场景下性能开销较大
可以通过MySQL的ON DUPLICATE KEY UPDATE语法优化
批量操作时建议使用批量查询减少数据库交互次数
最佳实践 :
合理选择主键策略,推荐使用雪花算法
大数据量操作时进行分批处理
添加适当的监控和异常处理
在并发场景下考虑使用乐观锁或分布式锁
(二)使用建议 ✅ 适用场景
数据同步和ETL操作
配置管理和缓存预热
小到中等数据量的批量操作
对数据一致性要求较高的场景
❌ 不适用场景
超大数据量的批量操作(建议使用专门的批量插入工具)
对性能要求极高的场景(建议使用原生SQL)
复杂的业务逻辑判断(建议自定义实现)
🔧 优化方向
使用数据库特性(如ON DUPLICATE KEY UPDATE)
实现智能批量查询减少数据库交互
添加缓存层减少重复查询
使用异步处理提升用户体验
MyBatis Plus的insertOrUpdate功能为开发者提供了便捷的数据保存方式,但在使用时需要充分理解其实现原理和性能特点。通过合理的优化策略和最佳实践,可以在保证功能正确性的同时获得良好的性能表现。
参考资料:
MyBatis Plus官方文档
MyBatis Plus源码分析
MySQL ON DUPLICATE KEY UPDATE语法
Java反射机制详解
Spring事务管理
数据库性能优化最佳实践