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

全部评论

相关推荐

2025-12-12 09:10
门头沟学院 Java
一面:&nbsp;1.&nbsp;自我介绍2.&nbsp;介绍两个项目中感觉比较有技术含量的点(防止超额报名,还有滑动窗口限流)3.&nbsp;活动报名场景,如果一天有几万个人报名怎么办,一小时几万人呢,可能想问redis的大key问题4.&nbsp;redis单线程为啥性能好,引出了IO多路复用模型的select、poll、epoll,为什么zset用跳表不用B+树或B树,跳表为啥性能好,追问为啥单线程而不是多线程5.&nbsp;假设想做一个排行榜,一个按分数排序,如果分数相同了按照报名时间排序,1万个数据里面想要找到top10应该怎么设计6.&nbsp;redis有哪些数据类型,然后redis&nbsp;的&nbsp;List&nbsp;类型做一个队列应该怎么做呢7.&nbsp;zset里面存报名数据应不应该清除,什么时候清呢?8.&nbsp;zSet与MySQL之间通过RocketMQ发数据,RocketMQ有持久化机制和重试机制,为什么还要有对账机制9.&nbsp;InnoDB有个AB联合索引,有个查询语句select&nbsp;B&nbsp;from&nbsp;这个表&nbsp;where&nbsp;A&nbsp;=&nbsp;1,走不走索引,回不回表,你觉得一定会走索引吗10.&nbsp;还是AB联合索引,update&nbsp;A&nbsp;=&nbsp;100&nbsp;where&nbsp;B&nbsp;=&nbsp;100,这条语句有没有什么问题?11.&nbsp;如果有个AService、里面有个方法A,方法B都有@Transactional注解,C需要引用A和B方法,事务会不会失效?12.&nbsp;CAS&nbsp;相对于直接加锁有什么好处?13.&nbsp;策略模式和状态模式最大的区别什么?14.&nbsp;接口是个抽象,咱们在使用接口的时候都需要自己写个实现类去实现接口,然后重写调用实现类才能执行逻辑,Mybatis为啥只有Mapper接口和XML就可以而不用写具体实现类的逻辑?15.&nbsp;为啥用了RocketMQ而不是Kafka16.&nbsp;怎么保证RocketMQ不丢失消息17.&nbsp;假设现在有个线程池,核心线程数&nbsp;2个,最大线程数100,阻塞队列100,来了100个任务,每个任务执行时间1s,多长时间执行完(50秒),那200个任务呢18.&nbsp;力扣92的反转链表II(10分钟没写完)二面(主管面):1.自我介绍2.项目中认为的难点3.项目做的背景是啥&nbsp;为啥要做这个项目4.最近学了什么5.了不了解大模型,Spring&nbsp;AI之类的6.&nbsp;兴趣爱好&nbsp;最有成就感的事&nbsp;能体现最有毅力的事是啥之类的
查看28道真题和解析
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

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