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圣经

全部评论

相关推荐

评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务