数据库表映射Java对象方案咨询及MyBatis缓存脏数据解决
咱们逐个解决你的两个问题,结合MyBatis的实际运行机制给出可行方案:
问题1:方案一的缓存脏数据问题
先解释下为什么会出现脏数据:你贴的代码里的localCache是MyBatis的一级缓存,它属于会话(SqlSession)级别——当你首次查询StudentCourse时,MyBatis会把关联查询出来的Student、Course连同StudentCourse对象一起存入这个缓存。如果在同一个SqlSession会话里,数据库的Course记录被更新了,再次查询StudentCourse时会直接命中缓存,拿到的就是更新前的旧数据,也就是你说的脏数据。
这里有几个实用的解决办法:
调整一级缓存的作用范围
把MyBatis的localCacheScope设置为STATEMENT,这样一级缓存只会在当前SQL语句执行期间有效,执行完就自动清空,不会跨语句保留缓存数据。你可以在MyBatis配置文件里添加:<settings> <setting name="localCacheScope" value="STATEMENT"/> </settings>或者针对特定的查询方法,用注解强制刷新缓存:
@Options(flushCache = Options.FlushCachePolicy.TRUE) List<StudentCourse> getStudentCourses();注意
flushCache=true会每次查询都清空缓存,对性能略有影响,适合数据实时性要求高的场景。手动清空会话缓存
在执行完Course的更新操作后,主动调用sqlSession.clearCache(),把当前会话的一级缓存清空。这样下次查询StudentCourse时,就会重新从数据库拉取最新的关联数据。利用二级缓存的刷新机制
如果你的项目开启了MyBatis二级缓存,可以给Student、Course和StudentCourse的Mapper配置共享缓存,并且在Course的更新方法上设置flushCache=true。这样当Course数据更新时,会自动清空相关的二级缓存,下次查询会重新加载最新数据。
👉 注意:二级缓存要求实体类必须实现Serializable接口,还要合理配置缓存的作用范围,避免出现其他缓存一致性问题。延迟加载关联对象
修改你的ResultMap,给association标签加上fetchType="lazy",让Student和Course对象在首次查询StudentCourse时不加载,只有当你实际调用studentCourse.getCourse()这类方法时,才会去数据库查询。这样如果中间Course数据更新了,调用时就能拿到最新的数据。
👉 小提示:这个方案可能会引入N+1查询问题,你可以结合MyBatis的lazyLoadingEnabled=true和aggressiveLazyLoading=false配置,平衡实时性和性能。
问题2:方案二的N+1查询问题
方案二的StudentCourse只存ID,确实容易触发N+1查询,但有几种办法可以优化:
联表查询+ResultMap映射
直接写一个联表查询的SQL,一次性把StudentCourse、Student和Course的数据查出来,然后用ResultMap把结果映射到你的StudentCourse对象(或者扩展一个包含关联对象的DTO)。举个例子:<select id="getStudentCourses" resultMap="studentCourseWithDetails"> SELECT sc.student_id, sc.course_id, sc.score, s.*, c.* FROM student_course sc JOIN student s ON sc.student_id = s.id JOIN course c ON sc.course_id = c.id </select> <resultMap id="studentCourseWithDetails" type="StudentCourse"> <result property="studentId" column="student_id"/> <result property="courseId" column="course_id"/> <result property="score" column="score"/> <association property="student" resultMap="Student"/> <!-- 复用已有的Student ResultMap --> <association property="course" resultMap="Course"/> <!-- 复用已有的Course ResultMap --> </resultMap>这样一次SQL查询就能拿到所有需要的数据,完全避免N+1问题。
批量查询+手动关联
分三步操作:先查询所有StudentCourse列表,然后收集所有的courseId,用IN语句批量查询Course数据,最后在业务层把Course对象和对应的StudentCourse关联起来。代码示例:// 1. 查询所有StudentCourse List<StudentCourse> studentCourses = studentCourseMapper.listAll(); // 2. 收集所有courseId Set<Long> courseIds = studentCourses.stream() .map(StudentCourse::getCourseId) .collect(Collectors.toSet()); // 3. 批量查询Course List<Course> courses = courseMapper.listByIds(courseIds); // 4. 手动关联 Map<Long, Course> courseMap = courses.stream() .collect(Collectors.toMap(Course::getId, Function.identity())); studentCourses.forEach(sc -> sc.setCourse(courseMap.get(sc.getCourseId())));这样只需要2次查询,比N+1高效太多。
嵌套查询+批量执行器
在MyBatis里配置嵌套查询,并且开启批量执行器(defaultExecutorType=BATCH)。比如在StudentCourse的ResultMap里配置:<resultMap id="studentCourse" type="StudentCourse"> <result property="studentId" column="student_id"/> <result property="courseId" column="course_id"/> <result property="score" column="score"/> <association property="course" column="course_id" select="com.example.mapper.CourseMapper.getById" fetchType="lazy"/> </resultMap>然后把MyBatis的默认执行器设置为BATCH,这样当批量加载
Course时,MyBatis会把多个查询合并成批量操作,减少数据库交互次数,缓解N+1的性能问题。
最后啰嗦一句:方案一和方案二各有优劣,你可以根据业务场景选择。如果想要避免N+1且对数据实时性要求高,方案一配合缓存优化是不错的选择;如果更倾向于轻量级实体类,方案二配合联表/批量查询也能解决性能问题。
内容的提问来源于stack exchange,提问作者Gloduck

