关于Mybatis的研究之 bean没有set方法 也可以赋值

本文探讨了MyBatis框架如何使用暴力反射机制为私有字段赋值,即使这些字段缺少setter方法也能正确填充结果集。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在学习mybatis的时候,发现了一个有趣的现象

如果resultType所指向的bean没有set方法时 在ibatis上是不能成功并且报错误 但在mybatis却是可以的

bean类:

sqlmap如下:



测试类代码如下:

[java]  view plain  copy
  1. @Test  
  2.     public void testNewTable() throws IOException{  
  3.         String resource = "sqlMap_config.xml";  
  4.         Reader reader = Resources.getResourceAsReader(resource);  
  5.         SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();  
  6.         SqlSessionFactory factory = builder.build(reader);  
  7.           
  8.         SqlSession session = factory.openSession();  
  9.         List<User> list = session.selectList("NewTable.selectAll");  
  10.           
  11.         System.out.println( list.get(0) );  
  12.           
  13.     }  

这样User类的属性居然是有值的!!!!

这着实让我很诧异 在排除了是我操作的问题的情况下,最终的造成此现象的根源在于mybatis本身。

于是我有了想看mybatis源码的冲动。。。。


2.分析问题

说实话,这是我第一次正儿八经的看框架源码。

一个字:杂啊!!!

但杂而不乱,我debug了好久才找到了根源

SetFieldInvoke

[java]  view plain  copy
  1. public class SetFieldInvoker implements Invoker {  
  2.   private Field field;  
  3.   
  4.   public SetFieldInvoker(Field field) {  
  5.     this.field = field;  
  6.   }  
  7.   
  8.   public Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException {  
  9.     field.set(target, args[0]);  
  10.     return null;  
  11.   }  
  12.   
  13.   public Class<?> getType() {  
  14.     return field.getType();  
  15.   }  
  16. }  

[java]  view plain  copy
  1. field.set(target, args[0]);  

原来它利用的是反射原理直接操作属性而非set方法


但有一个问题出现了 User类中的属性都是private的。如果 Field 类中没有setAccessible(true)的话,是不会访问到私有属性的。

在SetFieldInvoke方法中并没有执行此方法啊,但结果确实是给私有属性赋值了。

可以说明一点是setAccessible(true)此方法 肯定是执行了。

剩下的问题是此方法在哪里执行了??

于是我搜索文本setAccessible(true) 

果然在org.apache.ibatis.reflection.Reflector 中 发现了此方法在调用

[java]  view plain  copy
  1. private void addFields(Class<?> clazz) {  
  2.     Field[] fields = clazz.getDeclaredFields();  
  3.     for (Field field : fields) {  
  4.       if (canAccessPrivateMethods()) {  
  5.         try {  
  6.           field.setAccessible(true);  
  7.         } catch (Exception e) {  
  8.           // Ignored. This is only a final precaution, nothing we can do.  
  9.         }  
  10.       }  
  11.       if (field.isAccessible()) {  
  12.         if (!setMethods.containsKey(field.getName())) {  
  13.           // issue 379 - removed the check for final because JDK 1.5 allows  
  14.           // modification of final fields through reflection (JSR-133).  (JGB)  
  15.           addSetField(field);  
  16.         }  
  17.         if (!getMethods.containsKey(field.getName())) {  
  18.           addGetField(field);  
  19.         }  
  20.       }  
  21.     }  
  22.     if (clazz.getSuperclass() != null) {  
  23.       addFields(clazz.getSuperclass());  
  24.     }  
  25.   }  

简单来说,就是在初始化的时候就将每个属性都 setAccessible(true)
说明一点就是mybaits 的result结果集的映射用的是暴力反射 直接赋值;
<think>我们之前已经讨论了在Ruoyi框架中使用MyBatis Plus实现“无新增、已有数据不添加”的两种方案。现在,针对用户的问题,我们需要更详细地解释和扩展。 用户问题:ruoyi plus mybatis plus 对于 没有新增 已有数据不添加 理解:用户希望在插入数据时,如果数据已经存在(根据某个唯一标识),则不再插入;如果不存在,则插入。同时,用户可能希望这是一个高效且原子性的操作。 我们之前提供了两种方案: 方案一:使用MyBatis Plus的saveOrUpdate方法,通过查询条件判断是否存在,如果存在则执行更新(但更新内容为空,相当于不操作),不存在则插入。 方案二:利用数据库的唯一索引,通过INSERT ... ON DUPLICATE KEY UPDATE语句,在发生冲突时执行一个空操作(例如更新为原值),从而实现无新增。 但是,我们注意到在方案一中,我们使用了`saveOrUpdate(user, wrapper)`,并且解释为设置updateWrapper为空表示不更新已有数据。然而,在MyBatis Plus中,`saveOrUpdate(entity, wrapper)`方法实际上是根据wrapper查询是否存在记录,如果存在则更新,不存在则插入。但是,如果我们不指定更新内容,那么更新操作会更新所有字段(使用entity的非空字段)。这可能导致两个问题: 1. 如果entity中某些字段为null,那么更新时会将数据库中对应字段更新为null,这显然不符合要求。 2. 我们并不想更新已有数据,而只是希望忽略插入。 因此,我们需要调整方案一: 调整后的方案一:先查询,如果不存在则插入。 代码示例: ```java public boolean saveIfAbsent(SysUser user) { // 构建唯一查询条件 LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUsername, user.getUsername()); // 先查询 if (getOne(wrapper) != null) { return false; // 已存在,不插入 } return save(user); } ``` 但是,这样写存在并发问题:如果两个线程同时判断不存在,然后都执行插入,则可能会插入两条相同的数据(除非数据库有唯一索引约束)。因此,即使使用这种先查询再插入的方式,也需要数据库唯一索引来保证最终一致性。 所以,我们更推荐方案二,因为方案二在数据库层面保证了原子性,避免了并发问题。 然而,在Ruoyi框架中,我们通常使用MyBatis Plus,并且希望代码简洁。因此,我们可以结合方案二,使用自定义SQL,并利用数据库的唯一索引。 下面,我们详细说明方案二在Ruoyi中的实现步骤: 步骤1:在数据库表中创建唯一索引(如果已有则跳过) 例如,在sys_user表中,我们要确保username唯一: ```sql ALTER TABLE sys_user ADD UNIQUE INDEX uniq_username (username); ``` 步骤2:在对应的Mapper接口中编写自定义插入方法 使用MyBatis的注解方式或XML方式。这里以注解方式为例: ```java public interface SysUserMapper extends BaseMapper<SysUser> { @Insert("INSERT INTO sys_user (username, password, ...) " + "VALUES (#{username}, #{password}, ...) " + "ON DUPLICATE KEY UPDATE username = VALUES(username)") int insertOnDuplicateKeyNoChange(SysUser user); } ``` 注意:`ON DUPLICATE KEY UPDATE`后面我们设置了一个无变化的更新(例如将username更新为当前要插入的值,但由于冲突时username已经存在,所以实际上更新为原值)。这样,如果发生冲突,就执行一个无意义的更新,从而避免插入新记录,同时也不会改变原有记录。 但是,这种方法仍然会触发一次更新(虽然数据没变),会使得自增主键增加(如果使用自增主键),并且可能影响更新计数。如果我们不想做任何更新,可以更新一个无关紧要的字段,或者使用更巧妙的方法: 在MySQL中,我们可以更新一个常量值,比如: ```sql ON DUPLICATE KEY UPDATE username = username ``` 这样,更新操作不会改变任何数据,但会返回更新行数(受影响行数为1)。如果我们希望插入时返回1,冲突时返回0(表示没有插入新行),则可以使用以下方式: ```sql @Insert("INSERT INTO sys_user (username, password, ...) " + "VALUES (#{username}, #{password}, ...) " + "ON DUPLICATE KEY UPDATE id = id") int insertIgnoreOnDuplicate(SysUser user); ``` 这样,当发生冲突时,执行`id=id`(无变化),MySQL会返回受影响行数1(因为更新操作,即使数据没变,也会被认为是成功更新了1行)。所以,我们无法通过返回值判断是插入还是冲突。但是,我们可以通过返回的受影响行数判断:插入返回1,更新返回2(在MySQL中,如果使用ON DUPLICATE KEY UPDATE,插入返回1,更新返回2,更新但无变化返回1?实际上,MySQL文档说明:如果插入新行,受影响行数为1;如果更新行,受影响行数为2;如果更新前后相同,受影响行数为0(在MySQL 5.7+))。 具体行为取决于MySQL版本和配置。为了确保行为一致,我们可以使用以下方式: 在Service层,我们可以这样实现: ```java @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService { @Override public boolean saveIfAbsent(SysUser user) { try { int result = baseMapper.insertIgnoreOnDuplicate(user); // 如果返回1,表示插入成功;返回0,表示冲突(更新但无变化,在MySQL 5.7+中,无变化更新返回0) return result > 0; } catch (DuplicateKeyException e) { // 如果数据库没有唯一索引,但有其他唯一约束,或者我们使用其他方式,可能会捕获到唯一键冲突异常 return false; } } } ``` 但是,由于我们使用了ON DUPLICATE KEY UPDATE,所以不会抛出DuplicateKeyException,而是执行更新操作。因此,我们需要根据返回值判断。 在MySQL 5.7及以上版本,当使用ON DUPLICATE KEY UPDATE时,如果更新后的内容和更新前完全一样,那么受影响行数为0(不会更新)。但是,我们上面的SQL(id=id)并不会改变任何数据,所以受影响行数为0?实际上,MySQL对于更新操作,即使没有实际修改数据,也会返回1(受影响行数)。但是,在ON DUPLICATE KEY UPDATE中,如果插入则返回1,如果更新(无论是否修改)则返回2。所以,我们无法通过返回值判断是否插入了新行。 因此,我们可以修改SQL,在冲突时更新一个无关紧要的字段,比如一个时间戳(即使这样,我们也不希望改变原有数据)。或者,我们可以使用INSERT IGNORE,但INSERT IGNORE会忽略所有错误,包括其他错误,而且不会返回插入是否成功。 另一种做法:在ON DUPLICATE KEY UPDATE中,设置一个标记字段,例如: ```sql INSERT INTO ... ON DUPLICATE KEY UPDATE dummy_field = VALUES(dummy_field) ``` 这样,在冲突时更新一个字段,但该字段的值和原值相同,则不会改变数据(但会触发更新操作,返回2)。这样,我们就可以通过返回值判断: - 返回1:插入成功 - 返回2:冲突(已存在) 但是,我们并不希望更新任何字段。所以,我们可以在表中添加一个专门用于这种操作的字段,比如一个整数类型的dummy,默认值为0。然后,在更新时设置这个字段为0(和原值相同)。这样,即使更新,数据也没有变化,但MySQL会返回2(受影响行数)。 在MySQL中,更新操作即使没有实际修改数据,也会将受影响行数返回为1(在旧版本)或0(在5.7以上版本,如果设置innodb_autoinc_lock_mode=1或2,并且没有实际数据变化,则返回0)。具体行为复杂。 因此,我们建议:如果不需要精确判断是插入还是更新,只需要知道操作成功(即数据已经存在或插入成功),那么我们可以这样: 在Service中,我们调用这个方法,如果方法没有抛出异常,并且返回值大于0(表示插入成功或者冲突时更新成功),那么我们可以认为数据已经存在或已经插入。但是,我们无法区分是插入还是更新(无变化)。 但用户的需求是:已有数据不添加。所以,我们只关心是否插入了新数据。那么,我们如何知道是否插入了新数据? 我们可以通过以下方式: 1. 先查询,再插入(但并发问题需要唯一索引兜底)。 2. 在插入后,通过返回的主键值判断:如果插入成功,主键会被赋值;如果冲突,则主键不会被赋值(但MyBatis Plus的insert方法在插入成功后会自动填充主键,而在ON DUPLICATE KEY UPDATE中,如果冲突,则不会填充主键?因为实际没有插入新行)。所以,我们可以这样: ```java public boolean saveIfAbsent(SysUser user) { try { int affectedRows = baseMapper.insertOnDuplicateKeyNoChange(user); // 如果affectedRows为1,表示插入成功;如果为0,表示冲突且没有更新(无变化更新);如果为2,表示冲突且更新了(但我们不希望更新,所以应该避免) // 我们的SQL是ON DUPLICATE KEY UPDATE id=id,在MySQL 5.7+中,如果更新但无变化,则affectedRows=0;在旧版本可能是1。 // 因此,我们根据受影响行数判断:如果为1,则是插入;如果为0或2,则是冲突(已存在) // 但用户需求:已有数据不添加,所以我们只需要插入新数据,所以当插入成功(受影响行数1)返回true,否则false return affectedRows == 1; } catch (Exception e) { // 可能还有其他异常 return false; } } ``` 然而,受影响行数的不确定性使得这种方法不够健壮。 因此,我们推荐另一种做法:使用REPLACE INTO?但REPLACE INTO会先删除再插入,不符合要求。 或者,使用INSERT IGNORE: ```sql INSERT IGNORE INTO sys_user (username, ...) VALUES (...) ``` 这样,如果发生唯一键冲突,则忽略插入,并且不会报错。返回值:插入成功返回1,插入被忽略返回0。 所以,我们可以这样: ```java @Insert("INSERT IGNORE INTO sys_user (username, password, ...) " + "VALUES (#{username}, #{password}, ...)") int insertIgnore(SysUser user); ``` 然后在Service中: ```java public boolean saveIfAbsent(SysUser user) { int result = baseMapper.insertIgnore(user); return result > 0; // 如果插入成功,result=1;如果忽略,result=0 } ``` 这种方法简单明了,但需要注意: 1. INSERT IGNORE会忽略所有错误(不仅仅是唯一键冲突),所以如果其他错误(如数据类型错误)也会被忽略,返回0。因此,我们需要确保其他错误不会发生,或者通过异常捕获来处理。 2. 在插入被忽略时,不会产生错误,返回0。 所以,在业务上,我们只关心是否插入了新数据,所以用返回值>0判断即可。 综上所述,在Ruoyi框架中,我们推荐使用INSERT IGNORE方式,前提是数据库有唯一索引。步骤如下: 步骤1:确保数据库表有唯一索引(如username) 步骤2:在Mapper中定义insertIgnore方法 步骤3:在Service中调用,根据返回值判断 完整代码示例: **SysUserMapper.java** ```java public interface SysUserMapper extends BaseMapper<SysUser> { @Insert("INSERT IGNORE INTO sys_user (username, password, email, ...) " + "VALUES (#{username}, #{password}, #{email}, ...)") int insertIgnore(SysUser user); } ``` **SysUserServiceImpl.java** ```java @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService { @Override @Transactional public boolean saveIfAbsent(SysUser user) { return baseMapper.insertIgnore(user) > 0; } } ``` 注意:如果表字段很多,使用注解方式写SQL会很麻烦。可以使用XML方式,或者使用MyBatis Plus的SQL注入器,但这里为了简单,使用注解。 另外,如果字段很多,我们可以使用MyBatis的字段映射,但要注意:在INSERT IGNORE语句中,我们需要列出所有要插入的字段,并且和VALUES中的占位符一一对应。 如果使用XML,则可以在Mapper XML中写: ```xml <insert id="insertIgnore"> INSERT IGNORE INTO sys_user <trim prefix="(" suffix=")" suffixOverrides=","> <if test="username != null and username != ''">username,</if> <if test="password != null and password != ''">password,</if> ...其他字段 </trim> <trim prefix="VALUES (" suffix=")" suffixOverrides=","> <if test="username != null and username != ''">#{username},</if> <if test="password != null and password != ''">#{password},</if> ...其他字段 </trim> </insert> ``` 这样,可以动态生成SQL,避免null值问题。 总结:根据业务需求选择方案。如果业务并发不高,也可以使用先查询再插入的方式(注意加唯一索引兜底)。如果追求性能和简洁,INSERT IGNORE是较好的选择。 最后,我们提供一种在Ruoyi中完整的实现(使用INSERT IGNORE,并通过XML写SQL)。 步骤1:在Mapper接口中声明方法 ```java int insertIgnore(SysUser user); ``` 步骤2:在Mapper XML中写SQL(假设XML文件为SysUserMapper.xml) ```xml <insert id="insertIgnore"> INSERT IGNORE INTO sys_user (user_id, dept_id, user_name, nick_name, ...) VALUES (#{userId}, #{deptId}, #{userName}, #{nickName}, ...) </insert> ``` 注意:这里需要列出所有字段,包括主键。如果主键是自增的,那么可以省略主键列,但需要设置useGeneratedKeys为true(在Ruoyi中,通常主键是自增的,所以可以省略主键列,数据库自动生成)。但是,INSERT IGNORE在冲突时,自增主键会浪费一个值(所以自增主键会有空洞)。 因此,如果对自增主键的空洞不介意,可以使用。如果介意,则建议使用方案二(ON DUPLICATE KEY UPDATE)并更新一个无影响的字段,但这样也会浪费一个自增id(如果更新操作,自增主键不会增加,但REPLACE INTO会删除再插入,也会浪费自增主键)。 所以,在Ruoyi中,我们通常使用雪花算法生成主键(分布式ID),所以自增主键的问题不存在。Ruoyi默认使用MyBatis Plus的IdType.ASSIGN_ID(雪花算法),所以主键是程序生成的,不是自增的。因此,我们可以放心使用INSERT IGNORE。 步骤3:Service层调用 这样,我们就实现了“没有新增,已有数据不添加”的功能。 因此,最终方案:使用INSERT IGNORE,结合数据库唯一索引,并在Mapper中定义insertIgnore方法。 注意:在Ruoyi中,如果表结构发生变化,需要同步更新XML中的字段列表。 如果觉得维护字段列表麻烦,可以使用MyBatis Plus的SQL构建器,但这里不展开。 希望这个解决方案对你有帮助。 §§相关问题§§: 1. 在MySQL中,INSERT IGNORE和ON DUPLICATE KEY UPDATE有什么区别? 2. 如何避免在Ruoyi框架中手动编写大量字段的INSERT语句? 3. 在分布式系统中,如何实现插入数据的幂等性? 4. MyBatis Plus如何自定义SQL注入器来实现全局的insertIgnore方法?</think>### 优化方案:Ruoyi + MyBatis Plus 实现"无新增、已有数据不添加" 以下是更完善的解决方案,结合 Ruoyi 框架特性和 MyBatis Plus 的高级功能: #### 方案一:使用 MyBatis Plus 的 `saveOrUpdateBatch` 实现批量操作(推荐) ```java // Service 层实现 public boolean batchInsertIfAbsent(List<SysUser> userList) { // 1. 提取唯一标识(如用户名) List<String> usernames = userList.stream() .map(SysUser::getUsername) .collect(Collectors.toList()); // 2. 查询已存在的记录 Map<String, SysUser> existingUsers = list(new LambdaQueryWrapper<SysUser>() .in(SysUser::getUsername, usernames)) .stream() .collect(Collectors.toMap(SysUser::getUsername, Function.identity())); // 3. 过滤掉已存在的数据 List<SysUser> newUsers = userList.stream() .filter(user -> !existingUsers.containsKey(user.getUsername())) .collect(Collectors.toList()); // 4. 批量保存新数据 return saveOrUpdateBatch(newUsers); } ``` **代码说明**: 1. 先查询系统中已存在的记录 2. 使用 Java Stream API 过滤掉已存在的数据 3. 只保存真正的新数据 4. 避免了对已有记录的无效操作 #### 方案二:数据库层实现(MySQL 特有语法) ```sql <!-- 在 Mapper XML 文件中 --> <insert id="insertIfNotExists"> INSERT INTO sys_user (username, email, phone) SELECT #{username}, #{email}, #{phone} FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM sys_user WHERE username = #{username} ) </insert> ``` **Java 调用**: ```java // Mapper 接口 int insertIfNotExists(SysUser user); // Service 实现 public boolean safeInsert(SysUser user) { return userMapper.insertIfNotExists(user) > 0; } ``` **优势**: 1. 完全在数据库层实现,避免并发问题 2. 单次 SQL 操作,性能高效 3. 原子性保证,无需额外事务管理 #### 方案三:使用 MyBatis Plus 的 `InsertBatchSomeColumn` 扩展(最佳实践) 1. **安装扩展插件**: ```java @Configuration public class MybatisPlusConfig { @Bean public EasySqlInjector easySqlInjector() { return new EasySqlInjector(); } @Bean public InsertBatchSomeColumn insertBatchSomeColumn() { return new InsertBatchSomeColumn(); } } ``` 2. **自定义 Mapper 方法**: ```java public interface UserMapper extends BaseMapper<SysUser> { /** * 批量插入(忽略重复) */ @InsertProvider(type = InsertBatchSomeColumn.class, method = "insertBatchSomeColumn") int insertBatchSomeColumn(@Param("list") List<SysUser> userList); } ``` 3. **Service 层实现**: ```java public boolean safeBatchInsert(List<SysUser> userList) { // 去重逻辑 Map<String, SysUser> uniqueUsers = userList.stream() .collect(Collectors.toMap( SysUser::getUsername, Function.identity(), (existing, replacement) -> existing )); // 过滤已存在数据 List<SysUser> existingUsernames = list(new LambdaQueryWrapper<SysUser>() .in(SysUser::getUsername, uniqueUsers.keySet())); Set<String> existingSet = existingUsernames.stream() .map(SysUser::getUsername) .collect(Collectors.toSet()); List<SysUser> newUsers = uniqueUsers.values().stream() .filter(user -> !existingSet.contains(user.getUsername())) .collect(Collectors.toList()); // 批量插入 return userMapper.insertBatchSomeColumn(newUsers) > 0; } ``` ### 完整 Ruoyi 集成示例 **Controller 层**: ```java @PostMapping("/batchAdd") @ApiOperation("批量添加用户(忽略已存在)") public AjaxResult batchAdd(@RequestBody List<SysUser> userList) { try { if (userService.safeBatchInsert(userList)) { return AjaxResult.success("操作成功"); } return AjaxResult.error("未添加新数据"); } catch (DuplicateKeyException e) { return AjaxResult.error("数据重复:" + e.getMessage()); } } ``` **事务管理配置**: ```java @Service @Transactional(rollbackFor = Exception.class) public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements IUserService { // 实现代码... } ``` ### 性能优化建议 1. **批量操作分片处理**: ```java // 处理超大数据集(>1000条) public void largeBatchInsert(List<SysUser> userList) { List<List<SysUser>> partitions = Lists.partition(userList, 200); partitions.forEach(this::safeBatchInsert); } ``` 2. **Redis 缓存优化**: ```java // 使用 Redis 缓存已存在记录 private Set<String> getExistingUsernames(Set<String> usernames) { Set<String> existing = new HashSet<>(); Set<String> toQuery = new HashSet<>(); // 先查缓存 for (String username : usernames) { if (redisTemplate.hasKey("user:exists:" + username)) { existing.add(username); } else { toQuery.add(username); } } // 查询数据库 if (!toQuery.isEmpty()) { List<SysUser> dbExist = list(new LambdaQueryWrapper<SysUser>() .in(SysUser::getUsername, toQuery)); dbExist.forEach(user -> { existing.add(user.getUsername()); redisTemplate.opsForValue().set( "user:exists:" + user.getUsername(), "1", 1, TimeUnit.HOURS ); }); } return existing; } ``` 3. **异步处理**: ```java @Async public CompletableFuture<Boolean> asyncBatchInsert(List<SysUser> userList) { return CompletableFuture.completedFuture(safeBatchInsert(userList)); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值