Java秋招之MyBatis与数据访问
第7章 MyBatis与数据访问
面试重要程度:⭐⭐⭐⭐
常见提问方式:MyBatis缓存机制、动态SQL、插件原理
预计阅读时间:35分钟
开场白
兄弟,MyBatis作为Java生态中最流行的持久层框架,在面试中出现频率相当高。特别是它的缓存机制、动态SQL、插件开发等特性,经常被面试官拿来考察你对ORM框架的理解深度。
今天我们就把MyBatis的核心原理搞透,让你在面试中展现出对数据访问层的深度理解。
🏗️ 7.1 MyBatis核心组件
SqlSession生命周期
面试重点:
面试官:"说说MyBatis的核心组件,SqlSession的生命周期是怎样的?"
核心组件架构:
// 1. SqlSessionFactory - 会话工厂(单例) public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource()); factory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return factory.getObject(); } } // 2. SqlSession - 会话(线程不安全,需要每次创建) public class UserService { @Autowired private SqlSessionFactory sqlSessionFactory; public User findUserById(Long id) { // 手动管理SqlSession(不推荐) try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper mapper = sqlSession.getMapper(UserMapper.class); return mapper.selectById(id); } } } // 3. Mapper接口 - 映射器(推荐使用) @Mapper public interface UserMapper { User selectById(@Param("id") Long id); List<User> selectByCondition(@Param("condition") UserCondition condition); int insert(User user); int update(User user); int deleteById(@Param("id") Long id); }
SqlSession生命周期管理:
// Spring集成后的最佳实践 @Service public class UserService { // Spring自动管理SqlSession生命周期 @Autowired private UserMapper userMapper; @Transactional public void transferUser(Long fromId, Long toId, BigDecimal amount) { // 同一个事务中,使用同一个SqlSession User fromUser = userMapper.selectById(fromId); User toUser = userMapper.selectById(toId); fromUser.setBalance(fromUser.getBalance().subtract(amount)); toUser.setBalance(toUser.getBalance().add(amount)); userMapper.update(fromUser); userMapper.update(toUser); // 事务结束时,SqlSession自动关闭 } }
Mapper接口代理机制
面试深入:
面试官:"MyBatis是如何通过接口就能执行SQL的?底层原理是什么?"
代理机制实现:
// MapperProxyFactory - Mapper代理工厂 public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>(); public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { // JDK动态代理创建Mapper实例 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } } // MapperProxy - Mapper代理类 public class MapperProxy<T> implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // Object方法直接调用 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (method.isDefault()) { // 接口默认方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 获取MapperMethod并执行 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } } // MapperMethod - 封装方法调用 public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } return result; } }
🗄️ 7.2 缓存机制深入
一级缓存与二级缓存
面试高频:
面试官:"MyBatis的缓存机制是怎样的?一级缓存和二级缓存有什么区别?"
一级缓存(SqlSession级别):
@Test public void testFirstLevelCache() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 第一次查询,从数据库获取 User user1 = mapper.selectById(1L); System.out.println("第一次查询: " + user1); // 第二次查询,从一级缓存获取(同一个SqlSession) User user2 = mapper.selectById(1L); System.out.println("第二次查询: " + user2); System.out.println("是否为同一对象: " + (user1 == user2)); // true // 执行更新操作,一级缓存被清空 mapper.update(new User(1L, "新名称")); // 第三次查询,重新从数据库获取 User user3 = mapper.selectById(1L); System.out.println("更新后查询: " + user3); } }
二级缓存(Mapper级别):
<!-- 在Mapper.xml中开启二级缓存 --> <mapper namespace="com.example.mapper.UserMapper"> <!-- 开启二级缓存 --> <cache eviction="LRU" <!-- 缓存回收策略 --> flushInterval="60000" <!-- 刷新间隔60秒 --> size="512" <!-- 缓存对象数量 --> readOnly="false"/> <!-- 是否只读 --> <select id="selectById" resultType="User" useCache="true"> SELECT * FROM user WHERE id = #{id} </select> <!-- 不使用二级缓存 --> <select id="selectSensitiveData" resultType="User" useCache="false"> SELECT * FROM user WHERE id = #{id} </select> </mapper>
缓存配置详解:
// 自定义缓存实现 public class RedisCache implements Cache { private final String id; private final RedisTemplate<String, Object> redisTemplate; public RedisCache(String id) { this.id = id; this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class); } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { String redisKey = getRedisKey(key); redisTemplate.opsForValue().set(redisKey, value, 30, TimeUnit.MINUTES); } @Override public Object getObject(Object key) { String redisKey = getRedisKey(key); return redisTemplate.opsForValue().get(redisKey); } @Override public Object removeObject(Object key) { String redisKey = getRedisKey(key); Object value = redisTemplate.opsForValue().get(redisKey); redisTemplate.delete(redisKey); return value; } @Override public void clear() { Set<String> keys = redisTemplate.keys(id + ":*"); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } @Override public int getSize() { Set<String> keys = redisTemplate.keys(id + ":*"); return keys != null ? keys.size() : 0; } private String getRedisKey(Object key) { return id + ":" + key.toString(); } }
缓存失效策略:
// 缓存失效的几种情况 public class CacheInvalidationDemo { @Test public void testCacheInvalidation() { // 1. 执行增删改操作,缓存自动失效 userMapper.insert(new User("张三")); userMapper.update(new User(1L, "李四")); userMapper.deleteById(2L); // 2. 手动清除缓存 sqlSession.clearCache(); // 清除一级缓存 // 3. 跨SqlSession的二级缓存 try (SqlSession session1 = sqlSessionFactory.openSession()) { UserMapper mapper1 = session1.getMapper(UserMapper.class); mapper1.selectById(1L); // 查询并缓存 session1.commit(); // 提交后才会放入二级缓存 } try (SqlSession session2 = sqlSessionFactory.openSession()) { UserMapper mapper2 = session2.getMapper(UserMapper.class); mapper2.selectById(1L); // 从二级缓存获取 } } }
🔄 7.3 动态SQL与性能优化
动态SQL标签详解
面试重点:
面试官:"MyBatis的动态SQL有哪些标签?如何优化复杂的动态查询?"
核心动态SQL标签:
<mapper namespace="com.example.mapper.UserMapper"> <!-- 1. if标签 - 条件判断 --> <select id="selectByCondition" resultType="User"> SELECT * FROM user WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="age != null"> AND age = #{age} </if> <if test="status != null"> AND status = #{status} </if> </select> <!-- 2. where标签 - 智能处理WHERE条件 --> <select id="selectByConditionWithWhere" resultType="User"> SELECT * FROM user <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="age != null"> AND age = #{age} </if> <if test="status != null"> AND status = #{status} </if> </where> </select> <!-- 3. choose/when/otherwise - 多分支选择 --> <select id="selectByPriority" resultType="User"> SELECT * FROM user <where> <choose> <when test="id != null"> id = #{id} </when> <when test="email != null and email != ''"> email = #{email} </when> <when test="phone != null and phone != ''"> phone = #{phone} </when> <otherwise> status = 1 </otherwise>
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经