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圣经
查看3道真题和解析
