MyBatis框架详解
MyBatis是一款优秀的持久层框架,支持自定义SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集,通过简单的XML配置或注解,将接口与SQL语句进行映射,专注于SQL本身,赋予开发者更多的控制权和灵活性。
MyBatis = 灵活SQL + 简化JDBC + 优雅映射 + 动态查询
- 🚀 半自动ORM:手写SQL,自动映射结果集,控制与灵活并存
- 🎯 动态SQL:强大的条件判断、循环、分支能力,生成复杂查询
- 💡 优雅设计:接口与实现分离,仅需定义接口,无需实现类
- 🔧 可插拔架构:插件机制支持自定义拦截和处理,易于扩展
- 🛡️ 缓存支持:一级缓存与二级缓存,提升查询性能
1. MyBatis核心原理与架构
MyBatis是一个优秀的持久层框架,其核心设计理念是将SQL与代码分离,同时提供简单而强大的映射机制。理解MyBatis的架构和原理对于正确高效地使用它至关重要。
1.1 MyBatis工作原理
MyBatis的工作流程可以概括为以下几个关键步骤:
- 构建阶段
- 运行阶段
1// 1. 创建SqlSessionFactoryBuilder对象2SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();34// 2. 加载mybatis-config.xml配置文件5InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");67// 3. 构建SqlSessionFactory对象8SqlSessionFactory factory = builder.build(inputStream);在构建阶段,MyBatis会完成以下工作:
- 解析配置文件中的标签
- 创建Configuration对象
- 加载映射文件和注解配置
- 初始化内置组件
- 创建SqlSessionFactory实例
1// 1. 获取SqlSession2try (SqlSession session = factory.openSession()) {3 // 2. 获取Mapper接口4 UserMapper mapper = session.getMapper(UserMapper.class);5 6 // 3. 执行SQL7 User user = mapper.getUserById(1);8 9 // 4. 提交事务10 session.commit();11}在运行阶段,MyBatis会完成以下工作:
- 创建SqlSession实例
- 创建Executor执行器
- 创建StatementHandler处理SQL语句
- 参数映射和设置
- 执行SQL并获取结果
- 结果集映射转换
- 返回业务对象
MyBatis通过动态代理技术实现了只需定义接口而无需编写实现类的便利特性。当我们调用Mapper接口方法时,MyBatis会拦截该调用,找到对应的SQL语句并执行,然后将结果映射为指定的对象。
1.2 核心组件剖析
MyBatis的核心组件构成了其完整的功能体系,每个组件负责特定的功能:
| 核心组件 | 主要功能 | 生命周期 | 关键特性 |
|---|---|---|---|
| Configuration | 全局配置信息 | 全局唯一 | 包含所有配置和映射信息 |
| SqlSessionFactory | 创建SqlSession | 全局唯一 | 线程安全,可重用 |
| SqlSession | 提供SQL执行方法 | 请求级别 | 非线程安全,短暂 |
| Executor | 执行器 | 请求级别 | 维护缓存,执行SQL |
| StatementHandler | 处理SQL语句 | 方法级别 | 创建Statement对象 |
| ParameterHandler | 处理SQL参数 | 方法级别 | 设置预编译参数 |
| ResultSetHandler | 处理结果集映射 | 方法级别 | 将结果集转为对象 |
| TypeHandler | 类型转换 | 全局可用 | Java与JDBC类型转换 |
| MappedStatement | 映射语句 | 全局可用 | 封装SQL和映射信息 |
- SqlSessionFactory:单例模式,应用级生命周期
- SqlSession:非线程安全,需要在方法作用域内使用
- Mapper:由SqlSession创建的代理对象,依赖于SqlSession生命周期
1.3 执行流程详解
MyBatis的执行流程可以详细分解为以下步骤:
-
初始化阶段
- 解析配置文件创建Configuration对象
- 加载Mapper映射文件或注解
- 解析SQL语句创建MappedStatement对象
- 构建SqlSessionFactory实例
-
执行阶段
- 通过SqlSessionFactory创建SqlSession
- 通过SqlSession获取Mapper代理对象
- 调用Mapper方法执行SQL
- SqlSession将请求转交给Executor执行器
- Executor调用StatementHandler处理SQL语句
- ParameterHandler设置参数
- 执行SQL语句
- ResultSetHandler将结果集转换为对象
- 返回执行结果给调用者
1// 1. 读取MyBatis配置文件2String resource = "mybatis-config.xml";3InputStream inputStream = Resources.getResourceAsStream(resource);45// 2. 构建SqlSessionFactory6SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);78// 3. 获取SqlSession9try (SqlSession sqlSession = sqlSessionFactory.openSession()) {10 // 4. 获取Mapper代理对象11 UserMapper mapper = sqlSession.getMapper(UserMapper.class);12 13 // 5. 执行SQL14 User user = mapper.getUserById(1);15 16 // 6. 处理结果17 System.out.println(user);18 19 // 7. 提交事务20 sqlSession.commit();21}当调用mapper.getUserById(1)时,MyBatis内部会:
- 通过动态代理拦截该方法调用
- 根据方法签名找到对应的MappedStatement
- 创建SqlSource生成SQL语句
- 设置参数并执行SQL
- 将结果集映射为User对象
- 返回结果给调用者
2. 配置体系详解
MyBatis的配置体系是其灵活性和可扩展性的关键。全局配置文件是MyBatis配置的入口,包含了多种配置元素,可以根据不同环境和需求进行定制。
2.1 全局配置文件
MyBatis全局配置文件(通常命名为mybatis-config.xml)结构清晰,各元素有严格的顺序要求:
1<?xml version="1.0" encoding="UTF-8" ?>2<!DOCTYPE configuration3PUBLIC "-//mybatis.org//DTD Config 3.0//EN"4"http://mybatis.org/dtd/mybatis-3-config.dtd">5<configuration>6<!-- 引入外部属性文件 -->7<properties resource="db.properties">8 <property name="username" value="dev_user"/>9 <property name="password" value="dev_password"/>10</properties>1112<!-- 全局设置 -->13<settings>14 <setting name="cacheEnabled" value="true"/>15 <setting name="lazyLoadingEnabled" value="true"/>16 <setting name="mapUnderscoreToCamelCase" value="true"/>17</settings>1819<!-- 类型别名 -->20<typeAliases>21 <package name="com.example.model"/>22</typeAliases>2324<!-- 类型处理器 -->25<typeHandlers>26 <typeHandler handler="com.example.handler.JsonTypeHandler"/>27</typeHandlers>2829<!-- 插件 -->30<plugins>31 <plugin interceptor="com.example.plugin.PageInterceptor">32 <property name="dialect" value="mysql"/>33 </plugin>34</plugins>3536<!-- 环境配置 -->37<environments default="development">38 <environment id="development">39 <transactionManager type="JDBC"/>40 <dataSource type="POOLED">41 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>42 <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>43 <property name="username" value="root"/>44 <property name="password" value="password"/>45 </dataSource>46 </environment>47</environments>4849<!-- 数据库厂商标识 -->50<databaseIdProvider type="DB_VENDOR">51 <property name="MySQL" value="mysql"/>52 <property name="Oracle" value="oracle"/>53 <property name="SQL Server" value="sqlserver"/>54</databaseIdProvider>5556<!-- 映射器 -->57<mappers>58 <mapper resource="com/example/mapper/UserMapper.xml"/>59 <package name="com.example.mapper"/>60</mappers>61</configuration>配置元素必须按照上述顺序定义,否则MyBatis会抛出异常!
2.2 环境与数据源配置
MyBatis支持多环境配置,可以为不同的开发阶段(开发、测试、生产)配置不同的数据源和事务管理器。
- 环境配置
- 事务管理器
- 数据源配置
1<environments default="development">2 <!-- 开发环境 -->3 <environment id="development">4 <transactionManager type="JDBC"/>5 <dataSource type="POOLED">6 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>7 <property name="url" value="jdbc:mysql://localhost:3306/dev_db"/>8 <property name="username" value="dev_user"/>9 <property name="password" value="dev_password"/>10 </dataSource>11 </environment>12 13 <!-- 测试环境 -->14 <environment id="test">15 <transactionManager type="JDBC"/>16 <dataSource type="POOLED">17 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>18 <property name="url" value="jdbc:mysql://test-server:3306/test_db"/>19 <property name="username" value="test_user"/>20 <property name="password" value="test_password"/>21 </dataSource>22 </environment>23 24 <!-- 生产环境 -->25 <environment id="production">26 <transactionManager type="MANAGED"/>27 <dataSource type="JNDI">28 <property name="data_source" value="java:comp/env/jdbc/ProductionDB"/>29 </dataSource>30 </environment>31</environments>MyBatis支持两种类型的事务管理器:
- JDBC:直接使用JDBC的事务管理机制,依赖于数据库连接的commit和rollback
1<transactionManager type="JDBC"/>- MANAGED:将事务管理交给容器(如Spring或JEE容器)
1<transactionManager type="MANAGED">2 <property name="closeConnection" value="false"/>3</transactionManager>MyBatis内置了三种数据源类型:
- UNPOOLED:不使用连接池,每次请求时打开和关闭连接
1<dataSource type="UNPOOLED">2 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>3 <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>4 <property name="username" value="root"/>5 <property name="password" value="password"/>6</dataSource>- POOLED:使用MyBatis内置的简单数据库连接池
1<dataSource type="POOLED">2 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>3 <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>4 <property name="username" value="root"/>5 <property name="password" value="password"/>6 <property name="poolMaximumActiveConnections" value="10"/>7 <property name="poolMaximumIdleConnections" value="5"/>8</dataSource>- JNDI:通过JNDI查找数据源,主要用于应用服务器环境
1<dataSource type="JNDI">2 <property name="data_source" value="java:comp/env/jdbc/MyDataSource"/>3</dataSource>在实际项目中,可以通过在构建SqlSessionFactory时指定环境ID来选择使用哪个环境:
1String resource = "mybatis-config.xml";2InputStream inputStream = Resources.getResourceAsStream(resource);3// 使用production环境4SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream, "production");2.3 类型处理器与别名
类型别名和类型处理器是MyBatis简化代码和扩展类型转换能力的两个重要机制。
- 类型别名
- 类型处理器
类型别名为Java类型设置一个简短的名字,减少冗长类名的重复输入。
1<typeAliases>2 <!-- 单个别名定义 -->3 <typeAlias alias="User" type="com.example.model.User"/>4 5 <!-- 批量定义包下所有类的别名 -->6 <package name="com.example.model"/>7</typeAliases>MyBatis内置了常见Java类型的别名:
| 别名 | 映射的类型 |
|---|---|
| string | String |
| long | Long |
| int | Integer |
| boolean | Boolean |
| date | Date |
| decimal | BigDecimal |
| map | Map |
| list | List |
类型处理器用于Java类型与JDBC类型之间的转换,MyBatis内置了常见类型的处理器,也支持自定义:
1<typeHandlers>2 <!-- 注册单个类型处理器 -->3 <typeHandler handler="com.example.handler.JsonTypeHandler" javaType="Object" jdbcType="VARCHAR"/>4 5 <!-- 注册包中所有类型处理器 -->6 <package name="com.example.handler"/>7</typeHandlers>自定义类型处理器示例:
1public class JsonTypeHandler<T> extends BaseTypeHandler<T> {2 private Class<T> clazz;3 private ObjectMapper objectMapper = new ObjectMapper();4 5 public JsonTypeHandler(Class<T> clazz) {6 this.clazz = clazz;7 }8 9 @Override10 public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {11 try {12 ps.setString(i, objectMapper.writeValueAsString(parameter));13 } catch (JsonProcessingException e) {14 throw new SQLException("Error converting JSON", e);15 }16 }17 18 @Override19 public T getNullableResult(ResultSet rs, String columnName) throws SQLException {20 return parseJson(rs.getString(columnName));21 }22 23 @Override24 public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {25 return parseJson(rs.getString(columnIndex));26 }27 28 @Override29 public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {30 return parseJson(cs.getString(columnIndex));31 }32 33 private T parseJson(String json) throws SQLException {34 if (json == null) return null;35 try {36 return objectMapper.readValue(json, clazz);37 } catch (IOException e) {38 throw new SQLException("Error parsing JSON", e);39 }40 }41}2.4 插件与日志配置
MyBatis提供了强大的插件机制和灵活的日志配置,可以扩展核心功能并监控SQL执行情况。
- 插件配置
- 日志配置
MyBatis插件是通过拦截器模式实现的,可以拦截核心组件的方法调用:
1<plugins>2 <!-- 分页插件 -->3 <plugin interceptor="com.github.pagehelper.PageInterceptor">4 <property name="helperDialect" value="mysql"/>5 <property name="reasonable" value="true"/>6 </plugin>7 8 <!-- SQL性能监控插件 -->9 <plugin interceptor="com.example.plugin.SqlPerformanceInterceptor">10 <property name="slowSqlThreshold" value="1000"/>11 </plugin>12</plugins>自定义插件需要实现Interceptor接口,并通过@Intercepts注解指定拦截点:
1@Intercepts({2 @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),3 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})4})5public class SqlPerformanceInterceptor implements Interceptor {6 private long slowSqlThreshold;7 8 @Override9 public Object intercept(Invocation invocation) throws Throwable {10 MappedStatement ms = (MappedStatement) invocation.getArgs()[0];11 Object parameter = invocation.getArgs()[1];12 13 BoundSql boundSql = ms.getBoundSql(parameter);14 String sql = boundSql.getSql();15 16 long start = System.currentTimeMillis();17 Object result = invocation.proceed();18 long end = System.currentTimeMillis();19 long time = end - start;20 21 if (time > slowSqlThreshold) {22 String logMsg = String.format("Slow SQL: %s, Time: %dms", sql, time);23 System.err.println(logMsg);24 }25 26 return result;27 }28 29 @Override30 public Object plugin(Object target) {31 return Plugin.wrap(target, this);32 }33 34 @Override35 public void setProperties(Properties properties) {36 this.slowSqlThreshold = Long.parseLong(properties.getProperty("slowSqlThreshold", "1000"));37 }38}MyBatis支持多种日志框架,按照查找顺序排列:SLF4J → Log4j2 → Log4j → JDK logging
1<settings>2 <!-- 指定日志实现 -->3 <setting name="logImpl" value="SLF4J"/>4 5 <!-- 日志前缀 -->6 <setting name="logPrefix" value="MyBatis"/>7</settings>针对特定Mapper或语句的详细日志配置:
1<Loggers>2 <!-- 为特定Mapper设置日志级别 -->3 <Logger name="com.example.mapper.UserMapper" level="TRACE"/>4 5 <!-- 为所有MyBatis SQL日志设置级别 -->6 <Logger name="org.apache.ibatis" level="DEBUG"/>7</Loggers>通过设置日志级别为TRACE,可以看到以下详细信息:
- SQL语句
- 参数值
- 返回结果
- 性能统计
- 属性外部化:使用properties文件存储数据库连接信息
- 灵活环境配置:为不同环境(开发、测试、生产)配置不同数据源
- 启用驼峰命名映射:设置mapUnderscoreToCamelCase为true
- 合理使用缓存:根据业务需求配置二级缓存
- 适当的日志级别:开发环境使用DEBUG级别,生产环境使用INFO级别
3. 映射文件与SQL构建
MyBatis的核心功能是将SQL语句与Java方法关联起来,提供了XML和注解两种方式来定义SQL映射关系。这种分离式设计使得SQL语句和Java代码能够独立管理和优化。
3.1 XML映射文件详解
XML映射文件是MyBatis中定义SQL语句的主要方式,提供了丰富的配置选项和灵活的表达能力。
1<?xml version="1.0" encoding="UTF-8" ?>2<!DOCTYPE mapper3PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"4"http://mybatis.org/dtd/mybatis-3-mapper.dtd">5<mapper namespace="com.example.mapper.UserMapper">6<!-- 缓存配置 -->7<cache8 eviction="LRU"9 flushInterval="60000"10 size="512"11 readOnly="false"/>1213<!-- 可重用SQL片段 -->14<sql id="Base_Column_List">15 id, username, email, phone, create_time, update_time16</sql>1718<!-- 结果映射 -->19<resultMap id="UserResultMap" type="com.example.model.User">20 <id property="id" column="id" />21 <result property="username" column="username" />22 <result property="email" column="email" />23 <result property="phone" column="phone" />24 <result property="createTime" column="create_time" />25 <result property="updateTime" column="update_time" />26 <!-- 一对一关联 -->27 <association property="profile" javaType="com.example.model.UserProfile">28 <id property="id" column="profile_id" />29 <result property="address" column="address" />30 <result property="avatar" column="avatar" />31 </association>32 <!-- 一对多关联 -->33 <collection property="orders" ofType="com.example.model.Order">34 <id property="id" column="order_id" />35 <result property="orderNo" column="order_no" />36 <result property="amount" column="amount" />37 </collection>38</resultMap>3940<!-- 查询语句 -->41<select id="getUserById" resultMap="UserResultMap" parameterType="long">42 SELECT 43 u.*, 44 p.id as profile_id, 45 p.address, 46 p.avatar47 FROM user u48 LEFT JOIN user_profile p ON u.id = p.user_id49 WHERE u.id = #{id}50</select>5152<!-- 插入语句 -->53<insert id="insertUser" parameterType="com.example.model.User" useGeneratedKeys="true" keyProperty="id">54 INSERT INTO user (username, email, phone, create_time)55 VALUES (#{username}, #{email}, #{phone}, #{createTime})56</insert>5758<!-- 更新语句 -->59<update id="updateUser" parameterType="com.example.model.User">60 UPDATE user61 SET username = #{username},62 email = #{email},63 phone = #{phone},64 update_time = #{updateTime}65 WHERE id = #{id}66</update>6768<!-- 删除语句 -->69<delete id="deleteUser" parameterType="long">70 DELETE FROM user WHERE id = #{id}71</delete>72</mapper>- 查询语句
- 插入语句
- 更新语句
- 删除语句
查询语句是最常用的SQL操作,MyBatis提供了丰富的配置选项:
1<select 2 id="findUserByCondition" <!-- 唯一标识,对应Mapper接口方法名 -->3 parameterType="map" <!-- 参数类型 -->4 resultType="User" <!-- 结果类型 -->5 resultMap="UserResultMap" <!-- 结果映射引用 -->6 flushCache="false" <!-- 是否刷新缓存 -->7 useCache="true" <!-- 是否使用缓存 -->8 timeout="10000" <!-- 超时时间(毫秒) -->9 fetchSize="100" <!-- 结果集大小 -->10 statementType="PREPARED" <!-- 语句类型(STATEMENT/PREPARED/CALLABLE) -->11 resultSetType="FORWARD_ONLY"> <!-- 结果集类型 -->12 SELECT 13 <include refid="Base_Column_List"/>14 FROM user15 <where>16 <if test="username != null">17 username LIKE CONCAT('%', #{username}, '%')18 </if>19 <if test="email != null">20 AND email = #{email}21 </if>22 <if test="startTime != null">23 AND create_time >= #{startTime}24 </if>25 </where>26 ORDER BY create_time DESC27 LIMIT #{offset}, #{limit}28</select>插入语句用于向数据库添加记录,支持自动生成主键:
1<insert 2 id="insertUser" <!-- 唯一标识 -->3 parameterType="User" <!-- 参数类型 -->4 flushCache="true" <!-- 是否刷新缓存 -->5 timeout="20000" <!-- 超时时间(毫秒) -->6 useGeneratedKeys="true" <!-- 是否使用自动生成的主键 -->7 keyProperty="id" <!-- 自动生成的主键赋值给参数对象的属性 -->8 keyColumn="id"> <!-- 自动生成主键的列名 -->9 INSERT INTO user (10 username, 11 email, 12 password,13 status,14 create_time15 ) VALUES (16 #{username},17 #{email},18 #{password},19 #{status},20 #{createTime}21 )22</insert>2324<!-- 批量插入 -->25<insert id="batchInsertUsers" parameterType="list">26 INSERT INTO user (username, email, password, status, create_time)27 VALUES 28 <foreach collection="list" item="user" separator=",">29 (#{user.username}, #{user.email}, #{user.password}, #{user.status}, #{user.createTime})30 </foreach>31</insert>更新语句用于修改数据库记录:
1<update 2 id="updateUser" <!-- 唯一标识 -->3 parameterType="User" <!-- 参数类型 -->4 flushCache="true" <!-- 是否刷新缓存 -->5 timeout="20000"> <!-- 超时时间(毫秒) -->6 UPDATE user7 <set>8 <if test="username != null">username = #{username},</if>9 <if test="email != null">email = #{email},</if>10 <if test="password != null">password = #{password},</if>11 <if test="status != null">status = #{status},</if>12 update_time = #{updateTime}13 </set>14 WHERE id = #{id}15</update>1617<!-- 批量更新 -->18<update id="batchUpdateStatus" parameterType="map">19 UPDATE user20 SET status = #{status}21 WHERE id IN 22 <foreach collection="ids" item="id" open="(" separator="," close=")">23 #{id}24 </foreach>25</update>删除语句用于删除数据库记录:
1<delete 2 id="deleteUser" <!-- 唯一标识 -->3 parameterType="long" <!-- 参数类型 -->4 flushCache="true" <!-- 是否刷新缓存 -->5 timeout="20000"> <!-- 超时时间(毫秒) -->6 DELETE FROM user7 WHERE id = #{id}8</delete>910<!-- 批量删除 -->11<delete id="batchDeleteUsers" parameterType="list">12 DELETE FROM user13 WHERE id IN14 <foreach collection="list" item="id" open="(" separator="," close=")">15 #{id}16 </foreach>17</delete>3.2 注解映射方式
MyBatis支持使用Java注解来定义SQL映射,适合于简单的SQL语句,不需要XML文件。
1public interface UserMapper {2 3 @Select("SELECT * FROM user WHERE id = #{id}")4 User getUserById(Long id);5 6 @Insert("INSERT INTO user (username, email, phone, create_time) " +7 "VALUES (#{username}, #{email}, #{phone}, #{createTime})")8 @Options(useGeneratedKeys = true, keyProperty = "id")9 int insertUser(User user);10 11 @Update("UPDATE user SET username = #{username}, email = #{email}, " +12 "phone = #{phone}, update_time = #{updateTime} WHERE id = #{id}")13 int updateUser(User user);14 15 @Delete("DELETE FROM user WHERE id = #{id}")16 int deleteUser(Long id);17 18 // 结果映射19 @Select("SELECT * FROM user WHERE id = #{id}")20 @Results(id = "userResultMap", value = {21 @Result(property = "id", column = "id", id = true),22 @Result(property = "username", column = "username"),23 @Result(property = "email", column = "email"),24 @Result(property = "createTime", column = "create_time"),25 @Result(property = "updateTime", column = "update_time")26 })27 User getUserWithMapping(Long id);28 29 // 一对一关联30 @Select("SELECT u.*, p.id as profile_id, p.address, p.avatar FROM user u " +31 "LEFT JOIN user_profile p ON u.id = p.user_id WHERE u.id = #{id}")32 @Results({33 @Result(property = "id", column = "id", id = true),34 @Result(property = "username", column = "username"),35 @Result(property = "email", column = "email"),36 @Result(property = "profile", javaType = UserProfile.class, 37 column = "id", one = @One(select = "getProfileByUserId"))38 })39 User getUserWithProfile(Long id);40 41 @Select("SELECT * FROM user_profile WHERE user_id = #{userId}")42 UserProfile getProfileByUserId(Long userId);43 44 // 一对多关联45 @Select("SELECT * FROM user WHERE id = #{id}")46 @Results({47 @Result(property = "id", column = "id", id = true),48 @Result(property = "username", column = "username"),49 @Result(property = "orders", javaType = List.class, 50 column = "id", many = @Many(select = "getOrdersByUserId"))51 })52 User getUserWithOrders(Long id);53 54 @Select("SELECT * FROM orders WHERE user_id = #{userId}")55 List<Order> getOrdersByUserId(Long userId);56}3.3 两种映射方式对比
XML映射和注解映射各有优缺点,选择合适的方式取决于项目需求和团队偏好。
| 特性 | XML映射 | 注解映射 | 说明 |
|---|---|---|---|
| 可读性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | XML更适合大型复杂SQL |
| 维护性 | ⭐⭐⭐⭐ | ⭐⭐⭐ | XML分离SQL与代码,更易维护 |
| 动态SQL | ⭐⭐⭐⭐⭐ | ⭐⭐ | XML对动态SQL支持更全面 |
| 复杂映射 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | XML适合复杂结果映射 |
| 开发效率 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 注解简单快捷 |
| SQL重用 | ⭐⭐⭐⭐⭐ | ⭐⭐ | XML可提取公共SQL片段 |
| 编译时检查 | ⭐⭐ | ⭐⭐⭐⭐ | 注解可在编译时检查 |
| 适用场景 | 复杂SQL,多表关联 | 简单CRUD,单表操作 | - |
- 混合使用:根据SQL复杂度选择合适的方式
- 简单操作:单表的CRUD操作使用注解
- 复杂查询:多表关联、动态条件使用XML
- 团队统一:项目中保持一致的风格,避免混乱
3.4 SQL构建器使用
对于中等复杂度的SQL,MyBatis提供了SQL构建器API,可以通过Java代码动态构建SQL语句,比XML更灵活,比注解更强大。
1public interface UserMapper {2 3 @SelectProvider(type = UserSqlProvider.class, method = "findByCondition")4 List<User> findByCondition(UserQuery query);5 6 @InsertProvider(type = UserSqlProvider.class, method = "insert")7 @Options(useGeneratedKeys = true, keyProperty = "id")8 int insert(User user);9 10 @UpdateProvider(type = UserSqlProvider.class, method = "update")11 int update(User user);12 13 @DeleteProvider(type = UserSqlProvider.class, method = "deleteById")14 int deleteById(Long id);15}1617class UserSqlProvider {18 19 public String findByCondition(UserQuery query) {20 return new SQL() {{21 SELECT("id, username, email, phone, status, create_time, update_time");22 FROM("user");23 24 if (query.getUsername() != null) {25 WHERE("username LIKE CONCAT('%', #{username}, '%')");26 }27 28 if (query.getStatus() != null) {29 WHERE("status = #{status}");30 }31 32 if (query.getStartTime() != null) {33 WHERE("create_time >= #{startTime}");34 }35 36 if (query.getEndTime() != null) {37 WHERE("create_time <= #{endTime}");38 }39 40 ORDER_BY("create_time DESC");41 }}.toString();42 }43 44 public String insert(User user) {45 return new SQL() {{46 INSERT_INTO("user");47 VALUES("username", "#{username}");48 VALUES("email", "#{email}");49 VALUES("phone", "#{phone}");50 VALUES("password", "#{password}");51 VALUES("status", "#{status}");52 VALUES("create_time", "#{createTime}");53 }}.toString();54 }55 56 public String update(User user) {57 return new SQL() {{58 UPDATE("user");59 60 if (user.getUsername() != null) {61 SET("username = #{username}");62 }63 64 if (user.getEmail() != null) {65 SET("email = #{email}");66 }67 68 if (user.getPhone() != null) {69 SET("phone = #{phone}");70 }71 72 if (user.getStatus() != null) {73 SET("status = #{status}");74 }75 76 SET("update_time = #{updateTime}");77 WHERE("id = #{id}");78 }}.toString();79 }80 81 public String deleteById(Long id) {82 return new SQL() {{83 DELETE_FROM("user");84 WHERE("id = #{id}");85 }}.toString();86 }87}SQL构建器API的主要优势:
- 类型安全:使用Java代码构建SQL,可以在编译时检查
- 代码重用:可以封装常用的SQL片段为方法
- 灵活动态:比注解更灵活,比XML更直观
- 易于调试:可以在构建过程中添加日志或断点
SQL构建器的方法列表:
SELECT(String): 添加列到SELECT子句SELECT_DISTINCT(String): 添加DISTINCT列到SELECT子句FROM(String): 添加表到FROM子句JOIN(String): 添加JOIN子句INNER_JOIN(String): 添加INNER JOIN子句LEFT_OUTER_JOIN(String): 添加LEFT OUTER JOIN子句RIGHT_OUTER_JOIN(String): 添加RIGHT OUTER JOIN子句WHERE(String): 添加WHERE条件OR(): 添加OR连接符AND(): 添加AND连接符GROUP_BY(String): 添加GROUP BY子句HAVING(String): 添加HAVING条件ORDER_BY(String): 添加ORDER BY子句INSERT_INTO(String): 设置INSERT目标表VALUES(String, String): 添加INSERT值UPDATE(String): 设置UPDATE目标表SET(String): 添加SET子句DELETE_FROM(String): 设置DELETE目标表
4. 动态SQL与复杂查询
动态SQL是MyBatis最强大的特性之一,允许根据参数条件动态生成不同的SQL语句,极大地简化了复杂查询的构建过程。
4.1 条件查询构建
条件查询是最常见的动态SQL场景,MyBatis提供了多种标签来处理条件逻辑。
- if标签
- where标签
- choose标签
- trim标签
if标签是最基本的条件标签,根据测试条件决定是否包含标签体内的SQL片段:
1<select id="findUsers" resultType="User">2 SELECT * FROM user3 WHERE 1=14 <if test="username != null and username != ''">5 AND username LIKE CONCAT('%', #{username}, '%')6 </if>7 <if test="status != null">8 AND status = #{status}9 </if>10 <if test="startDate != null">11 AND create_time >= #{startDate}12 </if>13 <if test="endDate != null">14 AND create_time <= #{endDate}15 </if>16</select>where标签会智能处理条件语句,自动添加WHERE关键字,并去除多余的AND/OR前缀:
1<select id="findUsers" resultType="User">2 SELECT * FROM user3 <where>4 <if test="username != null and username != ''">5 username LIKE CONCAT('%', #{username}, '%')6 </if>7 <if test="status != null">8 AND status = #{status}9 </if>10 <if test="startDate != null">11 AND create_time >= #{startDate}12 </if>13 <if test="endDate != null">14 AND create_time <= #{endDate}15 </if>16 </where>17</select>choose标签类似于Java中的switch语句,提供多个条件中选择一个的能力:
1<select id="findUsersBySort" resultType="User">2 SELECT * FROM user3 <where>4 <if test="status != null">5 status = #{status}6 </if>7 </where>8 <choose>9 <when test="sortBy == 'username'">10 ORDER BY username11 </when>12 <when test="sortBy == 'createTime'">13 ORDER BY create_time14 </when>15 <otherwise>16 ORDER BY id17 </otherwise>18 </choose>19</select>trim标签提供了更灵活的前缀/后缀处理能力:
1<select id="findUsers" resultType="User">2 SELECT * FROM user3 <trim prefix="WHERE" prefixOverrides="AND|OR">4 <if test="username != null and username != ''">5 AND username LIKE CONCAT('%', #{username}, '%')6 </if>7 <if test="status != null">8 AND status = #{status}9 </if>10 </trim>11</select>OGNL表达式是MyBatis动态SQL的核心,用于条件测试和参数访问:
| OGNL表达式 | 描述 | 示例 |
|---|---|---|
| 属性访问 | 访问JavaBean属性 | username != null |
| 嵌套属性 | 访问嵌套属性 | user.address.city == 'Beijing' |
| 集合访问 | 访问List/Map元素 | users[0].name == 'admin' |
| 方法调用 | 调用对象方法 | username.length() > 5 |
| 静态方法 | 调用静态方法 | @java.lang.Math@max(id1, id2) |
| 逻辑运算符 | 逻辑组合 | age > 18 and age < 60 |
| 三元运算符 | 条件选择 | isVip ? price*0.8 : price |
4.2 循环与批量操作
foreach标签是处理集合参数的强大工具,适用于IN条件、批量插入等场景。
- IN条件查询
- 批量插入
- 批量更新
- 复杂参数处理
1<select id="findUsersByIds" resultType="User">2 SELECT * FROM user3 WHERE id IN4 <foreach collection="list" item="id" open="(" separator="," close=")">5 #{id}6 </foreach>7</select>1<insert id="batchInsert" parameterType="list">2 INSERT INTO user (username, email, status, create_time)3 VALUES4 <foreach collection="list" item="user" separator=",">5 (#{user.username}, #{user.email}, #{user.status}, #{user.createTime})6 </foreach>7</insert>1<update id="batchUpdate" parameterType="list">2 <foreach collection="list" item="item" separator=";">3 UPDATE user4 <set>5 username = #{item.username},6 email = #{item.email},7 update_time = #{item.updateTime}8 </set>9 WHERE id = #{item.id}10 </foreach>11</update>1<select id="findUsersByFilter" resultType="User">2 SELECT * FROM user3 <where>4 <if test="filter.nameList != null and filter.nameList.size() > 0">5 username IN6 <foreach collection="filter.nameList" item="name" open="(" separator="," close=")">7 #{name}8 </foreach>9 </if>10 <if test="filter.statusMap != null and filter.statusMap.size() > 0">11 AND12 <foreach collection="filter.statusMap" index="key" item="value" open="(" separator="OR" close=")">13 (${key} = #{value})14 </foreach>15 </if>16 </where>17</select>foreach标签的属性解析:
collection: 要迭代的集合名称(list、array、map等)item: 当前迭代的元素index: 当前迭代的索引(List)或键(Map)open: 拼接开始的字符串close: 拼接结束的字符串separator: 元素之间的分隔符
4.3 动态表名与列名
在某些场景下,需要动态指定表名或列名,MyBatis也提供了相应的解决方案。
动态表名和列名使用${}语法,会有SQL注入风险,必须确保参数来源安全可控!
- 动态表名
- 动态列名
- 安全处理方式
- 高级场景
1<select id="selectFromTable" resultType="map">2 SELECT * FROM ${tableName} WHERE id = #{id}3</select>1// 使用示例2Map<String, Object> data = mapper.selectFromTable("user_20230101", 1001);1<select id="getUserSortedBy" resultType="User">2 SELECT * FROM user ORDER BY ${columnName} ${order}3</select>1// 使用示例 - 必须确保参数值安全2List<User> users = mapper.getUserSortedBy("create_time", "DESC");对于动态表名和列名,建议使用白名单方式保证安全:
1public List<User> getUserSortedBy(String columnName, String order) {2 // 列名白名单3 Set<String> allowedColumns = new HashSet<>(Arrays.asList(4 "id", "username", "email", "create_time", "update_time"));5 6 // 排序方式白名单7 Set<String> allowedOrders = new HashSet<>(Arrays.asList("ASC", "DESC"));8 9 // 安全检查10 if (!allowedColumns.contains(columnName)) {11 columnName = "id"; // 默认值12 }13 14 if (!allowedOrders.contains(order.toUpperCase())) {15 order = "ASC"; // 默认值16 }17 18 return mapper.getUserSortedBy(columnName, order);19}对于更复杂的动态SQL需求,可以使用SQL构建器或自定义SQL提供者:
1@SelectProvider(type = DynamicQueryProvider.class, method = "buildQuery")2<T> List<T> executeCustomQuery(@Param("table") String table, 3 @Param("columns") String[] columns,4 @Param("conditions") Map<String, Object> conditions,5 @Param("resultType") Class<T> resultType);67public class DynamicQueryProvider {8 public String buildQuery(@Param("table") String table, 9 @Param("columns") String[] columns,10 @Param("conditions") Map<String, Object> conditions) {11 // 安全检查12 validateTable(table);13 validateColumns(columns);14 15 SQL sql = new SQL();16 17 // 构建查询列18 if (columns != null && columns.length > 0) {19 for (String column : columns) {20 sql.SELECT(column);21 }22 } else {23 sql.SELECT("*");24 }25 26 // 添加表名27 sql.FROM(table);28 29 // 添加条件30 if (conditions != null && !conditions.isEmpty()) {31 for (Map.Entry<String, Object> entry : conditions.entrySet()) {32 if (entry.getValue() != null) {33 sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");34 }35 }36 }37 38 return sql.toString();39 }40 41 private void validateTable(String table) {42 // 表名白名单检查43 Set<String> allowedTables = new HashSet<>(Arrays.asList(44 "user", "order", "product", "category"));45 if (!allowedTables.contains(table)) {46 throw new IllegalArgumentException("Invalid table name: " + table);47 }48 }49 50 private void validateColumns(String[] columns) {51 // 列名白名单检查52 if (columns == null) return;53 54 Set<String> allowedColumns = new HashSet<>(Arrays.asList(55 "id", "name", "price", "create_time", "update_time"));56 for (String column : columns) {57 if (!allowedColumns.contains(column)) {58 throw new IllegalArgumentException("Invalid column name: " + column);59 }60 }61 }62}4.4 多数据库支持
MyBatis提供了DatabaseIdProvider机制,可以根据不同数据库类型选择不同的SQL语句,实现跨数据库兼容。
首先在配置文件中定义数据库厂商标识:
1<databaseIdProvider type="DB_VENDOR">2 <property name="MySQL" value="mysql"/>3 <property name="Oracle" value="oracle"/>4 <property name="SQL Server" value="sqlserver"/>5 <property name="PostgreSQL" value="postgresql"/>6</databaseIdProvider>然后在Mapper XML中使用databaseId属性区分不同数据库的SQL:
1<!-- MySQL版本 -->2<select id="findUsers" resultType="User" databaseId="mysql">3 SELECT * FROM user4 <where>5 <if test="createDate != null">6 AND DATE(create_time) = #{createDate}7 </if>8 </where>9 LIMIT #{offset}, #{limit}10</select>1112<!-- Oracle版本 -->13<select id="findUsers" resultType="User" databaseId="oracle">14 SELECT * FROM 15 (16 SELECT ROWNUM rn, u.* FROM user u17 <where>18 <if test="createDate != null">19 AND TRUNC(create_time) = #{createDate}20 </if>21 </where>22 WHERE ROWNUM <= #{offset} + #{limit}23 )24 WHERE rn > #{offset}25</select>2627<!-- SQL Server版本 -->28<select id="findUsers" resultType="User" databaseId="sqlserver">29 SELECT * FROM user30 <where>31 <if test="createDate != null">32 AND CONVERT(date, create_time) = #{createDate}33 </if>34 </where>35 ORDER BY id36 OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY37</select>针对不同数据库的主要差异处理:
| 功能 | MySQL | Oracle | SQL Server | PostgreSQL |
|---|---|---|---|---|
| 分页查询 | LIMIT offset, limit | ROWNUM 或 ROW_NUMBER() | OFFSET-FETCH | LIMIT-OFFSET |
| 日期处理 | DATE(field) | TRUNC(field) | CONVERT(date, field) | field::date |
| 字符串连接 | CONCAT() | || | + 或 CONCAT() | || |
| 自增主键 | AUTO_INCREMENT | SEQUENCE | IDENTITY | SERIAL |
| 批量插入 | 多值语法 | 联合查询 | 表变量 | 多值语法 |
| NULL排序 | IS NULL DESC | NULLS LAST | CASE WHEN | NULLS LAST |
- 抽象公共SQL:将共同部分提取为SQL片段
- 数据库版本检测:根据databaseId选择适当的SQL
- 封装特殊处理:将数据库特定功能封装到辅助方法
- 分页插件:使用PageHelper等插件统一分页处理
- 方言配置:通过配置文件管理不同数据库方言
5. 结果映射与关联查询
结果映射是MyBatis的核心功能之一,它负责将查询结果集转换为Java对象,处理属性名与列名的映射关系,以及管理复杂的关联对象映射。
5.1 基础结果映射
基础结果映射处理简单的列名到属性名的映射,MyBatis提供了自动映射和手动配置两种方式。
- 自动映射
- 手动映射
当Java属性名与数据库列名匹配或符合驼峰命名转换规则时,MyBatis可以自动映射:
1<!-- 启用驼峰命名转换 (mybatis-config.xml) -->2<settings>3 <setting name="mapUnderscoreToCamelCase" value="true"/>4</settings>56<!-- 使用自动映射 -->7<select id="getUser" resultType="com.example.model.User">8 SELECT 9 id,10 username,11 email,12 phone,13 create_time, <!-- 会自动映射到createTime属性 -->14 update_time <!-- 会自动映射到updateTime属性 -->15 FROM user16 WHERE id = #{id}17</select>1public class User {2 private Long id;3 private String username;4 private String email;5 private String phone;6 private Date createTime; // 对应create_time列7 private Date updateTime; // 对应update_time列8 9 // getter和setter10}当列名与属性名不匹配或需要特殊处理时,可以使用resultMap进行手动映射:
1<!-- 定义结果映射 -->2<resultMap id="UserResultMap" type="com.example.model.User">3 <id property="id" column="user_id" /> <!-- 主键映射 -->4 <result property="username" column="user_name" /> <!-- 普通属性映射 -->5 <result property="email" column="user_email" />6 <result property="phoneNumber" column="phone" />7 <result property="createTime" column="gmt_create" />8 <result property="updateTime" column="gmt_modified" />9</resultMap>1011<!-- 使用结果映射 -->12<select id="getUser" resultMap="UserResultMap">13 SELECT 14 user_id,15 user_name,16 user_email,17 phone,18 gmt_create,19 gmt_modified20 FROM t_user21 WHERE user_id = #{id}22</select>resultMap属性详解
| 属性 | 描述 | 用法示例 |
|---|---|---|
| id | 主键映射,用于区分相同ID的对象 | <id property="id" column="user_id"/> |
| result | 普通属性映射 | <result property="username" column="user_name"/> |
| constructor | 使用构造函数注入属性 | <constructor><idArg/><arg/></constructor> |
| association | 一对一关联映射 | <association property="profile" javaType="UserProfile"/> |
| collection | 一对多关联映射 | <collection property="orders" ofType="Order"/> |
| discriminator | 鉴别器映射,根据列值选择映射方式 | <discriminator javaType="int" column="type"/> |
类型转换
MyBatis会自动处理Java类型与数据库类型之间的转换,也可以通过typeHandler指定自定义类型处理:
1<resultMap id="UserMap" type="User">2 <id property="id" column="id"/>3 <result property="createdAt" column="created_at" 4 typeHandler="com.example.handler.LocalDateTimeHandler"/>5 <result property="status" column="status" 6 typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>7 <result property="settings" column="settings" 8 typeHandler="com.example.handler.JsonTypeHandler"/>9</resultMap>5.2 一对一关联查询
MyBatis提供了强大的一对一关联查询能力,支持嵌套结果和嵌套查询两种方式。
- 嵌套结果映射
- 嵌套查询
- 复合主键关联
- 自动映射组合
嵌套结果映射通过一次SQL查询获取主对象和关联对象的所有数据:
1<resultMap id="UserWithProfileMap" type="User">2 <id property="id" column="user_id"/>3 <result property="username" column="username"/>4 <result property="email" column="email"/>5 6 <!-- 嵌套结果映射 - 一对一关联 -->7 <association property="profile" javaType="UserProfile">8 <id property="id" column="profile_id"/>9 <result property="address" column="address"/>10 <result property="phone" column="phone"/>11 <result property="avatar" column="avatar"/>12 </association>13</resultMap>1415<select id="getUserWithProfile" resultMap="UserWithProfileMap">16 SELECT 17 u.id as user_id,18 u.username,19 u.email,20 p.id as profile_id,21 p.address,22 p.phone,23 p.avatar24 FROM user u25 LEFT JOIN user_profile p ON u.id = p.user_id26 WHERE u.id = #{id}27</select>嵌套查询通过执行额外的SQL语句来加载关联对象:
1<resultMap id="UserMap" type="User">2 <id property="id" column="id"/>3 <result property="username" column="username"/>4 <result property="email" column="email"/>5 6 <!-- 嵌套查询 - 一对一关联 -->7 <association property="profile" 8 column="id" 9 select="getProfileByUserId"10 fetchType="lazy"/>11</resultMap>1213<select id="getUser" resultMap="UserMap">14 SELECT id, username, email FROM user WHERE id = #{id}15</select>1617<select id="getProfileByUserId" resultType="UserProfile">18 SELECT id, address, phone, avatar 19 FROM user_profile20 WHERE user_id = #{userId}21</select>处理复合主键关联的情况:
1<resultMap id="OrderResultMap" type="Order">2 <id property="id" column="order_id"/>3 <result property="orderNo" column="order_no"/>4 <result property="createTime" column="create_time"/>5 6 <!-- 复合主键关联 -->7 <association property="shippingAddress" javaType="Address">8 <id property="orderId" column="order_id"/>9 <id property="type" column="address_type"/>10 <result property="street" column="street"/>11 <result property="city" column="city"/>12 <result property="country" column="country"/>13 <result property="zipCode" column="zip_code"/>14 </association>15</resultMap>1617<select id="getOrderWithAddress" resultMap="OrderResultMap">18 SELECT19 o.id as order_id,20 o.order_no,21 o.create_time,22 a.type as address_type,23 a.street,24 a.city,25 a.country,26 a.zip_code27 FROM orders o28 LEFT JOIN address a ON o.id = a.order_id AND a.type = 'SHIPPING'29 WHERE o.id = #{id}30</select>结合自动映射和手动映射的混合使用:
1<resultMap id="UserWithProfileMap" type="User" autoMapping="true">2 <id property="id" column="id"/>3 <!-- 其他User属性自动映射 -->4 5 <association property="profile" javaType="UserProfile" autoMapping="true">6 <id property="id" column="profile_id"/>7 <!-- 其他Profile属性自动映射,前缀相同 -->8 </association>9</resultMap>1011<select id="getUserWithProfile" resultMap="UserWithProfileMap">12 SELECT 13 u.id,14 u.username,15 u.email,16 u.create_time,17 p.id AS profile_id,18 p.address AS profile_address,19 p.phone AS profile_phone20 FROM user u21 LEFT JOIN user_profile p ON u.id = p.user_id22 WHERE u.id = #{id}23</select>5.3 一对多关联查询
一对多关联处理一个对象关联多个子对象的情况,例如用户拥有多个订单。
- 嵌套结果映射
- 嵌套查询
- 多级嵌套
嵌套结果映射方式加载一对多关联:
1<resultMap id="UserWithOrdersMap" type="User">2 <id property="id" column="user_id"/>3 <result property="username" column="username"/>4 <result property="email" column="email"/>5 6 <!-- 一对多关联 -->7 <collection property="orders" ofType="Order">8 <id property="id" column="order_id"/>9 <result property="orderNo" column="order_no"/>10 <result property="amount" column="amount"/>11 <result property="createTime" column="order_create_time"/>12 <result property="status" column="order_status"/>13 </collection>14</resultMap>1516<select id="getUserWithOrders" resultMap="UserWithOrdersMap">17 SELECT 18 u.id as user_id,19 u.username,20 u.email,21 o.id as order_id,22 o.order_no,23 o.amount,24 o.create_time as order_create_time,25 o.status as order_status26 FROM user u27 LEFT JOIN orders o ON u.id = o.user_id28 WHERE u.id = #{id}29 ORDER BY o.create_time DESC30</select>嵌套查询方式加载一对多关联:
1<resultMap id="UserMap" type="User">2 <id property="id" column="id"/>3 <result property="username" column="username"/>4 <result property="email" column="email"/>5 6 <!-- 一对多嵌套查询 -->7 <collection property="orders" 8 column="id" 9 select="getOrdersByUserId"10 fetchType="lazy"/>11</resultMap>1213<select id="getUser" resultMap="UserMap">14 SELECT id, username, email FROM user WHERE id = #{id}15</select>1617<select id="getOrdersByUserId" resultType="Order">18 SELECT id, order_no, amount, create_time, status19 FROM orders20 WHERE user_id = #{userId}21 ORDER BY create_time DESC22</select>处理多级嵌套关联的情况:
1<resultMap id="UserWithOrdersAndItemsMap" type="User">2 <id property="id" column="user_id"/>3 <result property="username" column="username"/>4 5 <!-- 一级嵌套:用户的订单 -->6 <collection property="orders" ofType="Order">7 <id property="id" column="order_id"/>8 <result property="orderNo" column="order_no"/>9 <result property="amount" column="amount"/>10 11 <!-- 二级嵌套:订单的商品项 -->12 <collection property="items" ofType="OrderItem">13 <id property="id" column="item_id"/>14 <result property="productId" column="product_id"/>15 <result property="quantity" column="quantity"/>16 <result property="price" column="price"/>17 18 <!-- 三级嵌套:商品信息 -->19 <association property="product" javaType="Product">20 <id property="id" column="product_id"/>21 <result property="name" column="product_name"/>22 <result property="image" column="product_image"/>23 </association>24 </collection>25 </collection>26</resultMap>2728<select id="getUserWithOrdersAndItems" resultMap="UserWithOrdersAndItemsMap">29 SELECT 30 u.id as user_id,31 u.username,32 o.id as order_id,33 o.order_no,34 o.amount,35 i.id as item_id,36 i.product_id,37 i.quantity,38 i.price,39 p.name as product_name,40 p.image as product_image41 FROM user u42 LEFT JOIN orders o ON u.id = o.user_id43 LEFT JOIN order_item i ON o.id = i.order_id44 LEFT JOIN product p ON i.product_id = p.id45 WHERE u.id = #{id}46 ORDER BY o.create_time DESC, i.id ASC47</select>- N+1问题:嵌套查询可能导致大量额外查询,降低性能
- 延迟加载:适当使用fetchType="lazy"可以按需加载关联对象
- 结果集过大:复杂嵌套结果映射可能返回大量冗余数据
- 内存消耗:多级嵌套映射会消耗更多内存资源
5.4 鉴别器映射
鉴别器映射允许根据某列的值来决定如何映射结果集,类似于Java中的switch语句。
- 基础用法
- 嵌套映射
- 自定义类型映射
根据类型字段选择不同的映射方式:
1<resultMap id="VehicleResultMap" type="Vehicle">2 <id property="id" column="id"/>3 <result property="name" column="name"/>4 <result property="price" column="price"/>5 6 <!-- 根据vehicle_type列值确定映射方式 -->7 <discriminator javaType="int" column="vehicle_type">8 <case value="1" resultType="Car">9 <result property="engineType" column="engine_type"/>10 <result property="doors" column="doors"/>11 </case>12 <case value="2" resultType="Motorcycle">13 <result property="engineCapacity" column="engine_capacity"/>14 <result property="hasSideCar" column="has_side_car"/>15 </case>16 <case value="3" resultType="Bicycle">17 <result property="frameType" column="frame_type"/>18 <result property="gears" column="gears"/>19 </case>20 </discriminator>21</resultMap>2223<select id="getVehicleById" resultMap="VehicleResultMap">24 SELECT 25 id, name, price, vehicle_type,26 engine_type, doors, engine_capacity,27 has_side_car, frame_type, gears28 FROM vehicle29 WHERE id = #{id}30</select>鉴别器结合嵌套映射的复杂用法:
1<resultMap id="PaymentResultMap" type="Payment">2 <id property="id" column="payment_id"/>3 <result property="amount" column="amount"/>4 <result property="paymentDate" column="payment_date"/>5 6 <!-- 根据支付方式确定映射方式 -->7 <discriminator javaType="string" column="payment_type">8 <case value="CREDIT_CARD" resultMap="CreditCardPaymentMap"/>9 <case value="PAYPAL" resultMap="PayPalPaymentMap"/>10 <case value="BANK_TRANSFER" resultMap="BankTransferPaymentMap"/>11 <case value="CASH" resultMap="CashPaymentMap"/>12 </discriminator>13</resultMap>1415<!-- 信用卡支付映射 -->16<resultMap id="CreditCardPaymentMap" type="CreditCardPayment" extends="PaymentResultMap">17 <result property="cardNumber" column="card_number"/>18 <result property="expiryDate" column="expiry_date"/>19 <result property="cardType" column="card_type"/>20</resultMap>2122<!-- PayPal支付映射 -->23<resultMap id="PayPalPaymentMap" type="PayPalPayment" extends="PaymentResultMap">24 <result property="emailAddress" column="email_address"/>25 <result property="accountId" column="account_id"/>26</resultMap>2728<!-- 银行转账映射 -->29<resultMap id="BankTransferPaymentMap" type="BankTransferPayment" extends="PaymentResultMap">30 <result property="bankName" column="bank_name"/>31 <result property="accountNumber" column="account_number"/>32 <result property="referenceNumber" column="reference_number"/>33</resultMap>3435<!-- 现金支付映射 -->36<resultMap id="CashPaymentMap" type="CashPayment" extends="PaymentResultMap">37 <result property="receiptNumber" column="receipt_number"/>38</resultMap>鉴别器结合类型处理器的高级用法:
1<resultMap id="ProductResultMap" type="Product">2 <id property="id" column="id"/>3 <result property="name" column="name"/>4 <result property="price" column="price"/>5 <result property="categoryId" column="category_id"/>6 7 <!-- 根据产品类型决定如何处理attributes字段 -->8 <discriminator javaType="string" column="product_type">9 <case value="ELECTRONIC" resultType="ElectronicProduct">10 <result property="attributes" column="attributes" 11 typeHandler="com.example.handler.ElectronicAttributesHandler"/>12 </case>13 <case value="CLOTHING" resultType="ClothingProduct">14 <result property="attributes" column="attributes" 15 typeHandler="com.example.handler.ClothingAttributesHandler"/>16 </case>17 <case value="FOOD" resultType="FoodProduct">18 <result property="attributes" column="attributes" 19 typeHandler="com.example.handler.FoodAttributesHandler"/>20 <result property="expiryDate" column="expiry_date"/>21 </case>22 </discriminator>23</resultMap>鉴别器映射的主要适用场景:
- 继承关系映射:处理不同子类的差异化字段
- 多态对象处理:根据类型字段返回不同类型的对象
- 条件字段映射:根据状态决定如何处理某些字段
- 复杂业务逻辑:根据业务规则选择不同的映射策略
- 优先使用自动映射:当列名符合命名规则时,启用自动映射
- 组合使用策略:根据复杂度选择合适的映射方式
- 延迟加载:使用fetchType="lazy"避免不必要的数据加载
- 结果缓存:为复杂查询启用缓存,提高性能
- 预加载关联:对于经常一起使用的关联数据,使用嵌套结果映射
6. 缓存机制与性能优化
MyBatis提供了完善的缓存机制,通过合理使用缓存,可以显著提升查询性能,减少数据库访问次数。MyBatis的缓存分为一级缓存和二级缓存两个层次。
6.1 一级缓存机制
一级缓存是SqlSession级别的缓存,默认启用,生命周期仅限于单个SqlSession。
- 基本原理
- 清除缓存
- 配置选项
- 使用考量
一级缓存的工作原理:
- 作用范围:SqlSession级别,同一个SqlSession中的相同查询会使用缓存
- 缓存键:基于Statement ID + SQL + 参数值 + RowBounds计算
- 自动管理:默认开启,无需额外配置
- 生命周期:SqlSession关闭后缓存失效
1// 获取SqlSession2try (SqlSession session = sqlSessionFactory.openSession()) {3 UserMapper mapper = session.getMapper(UserMapper.class);4 5 // 第一次查询,访问数据库6 User user1 = mapper.getUserById(1001L);7 8 // 第二次查询,使用一级缓存9 User user2 = mapper.getUserById(1001L);10 11 // user1和user2是同一个对象引用12 System.out.println(user1 == user2); // true13}一级缓存在以下情况会被清除:
- 调用
SqlSession.clearCache()方法 - 执行UPDATE、INSERT、DELETE等修改操作
- 调用带有
flushCache=true属性的查询 - SqlSession执行提交或回滚操作
- SqlSession关闭
1try (SqlSession session = sqlSessionFactory.openSession()) {2 UserMapper mapper = session.getMapper(UserMapper.class);3 4 // 第一次查询5 User user1 = mapper.getUserById(1001L);6 7 // 清除一级缓存8 session.clearCache();9 10 // 第二次查询,会再次访问数据库11 User user2 = mapper.getUserById(1001L);12 13 // user1和user2不是同一个对象14 System.out.println(user1 == user2); // false15}1<!-- flushCache=true会清除一级缓存和二级缓存 -->2<select id="getUserByEmail" resultType="User" flushCache="true">3 SELECT * FROM user WHERE email = #{email}4</select>一级缓存的配置选项:
1<settings>2 <!-- 设置一级缓存的作用范围:3 SESSION (默认): 同一个SqlSession内有效4 STATEMENT: 语句级别,相当于禁用一级缓存5 -->6 <setting name="localCacheScope" value="SESSION"/>7</settings>当localCacheScope设为STATEMENT时,一级缓存仅对单个语句有效,每次查询完成后都会清空缓存,相当于禁用了一级缓存。
一级缓存使用注意事项:
- 适用场景:适用于单个方法内多次查询相同数据的场景
- 事务隔离:一级缓存不跨SqlSession,不会有数据一致性问题
- 对象一致性:同一SqlSession内相同查询返回的是同一个对象引用
- 性能影响:通常有助于减少重复查询,但对整体性能影响有限
- 安全关注点:在并发环境中,如果共享SqlSession可能导致数据混乱
1// 正确使用方式 - 每个线程独立的SqlSession2@Service3public class UserService {4 @Autowired5 private SqlSessionFactory sqlSessionFactory;6 7 public User getUserById(Long id) {8 try (SqlSession session = sqlSessionFactory.openSession()) {9 UserMapper mapper = session.getMapper(UserMapper.class);10 return mapper.getUserById(id);11 }12 }13 14 // 在同一个事务中使用一级缓存15 public void processUserData(Long id) {16 try (SqlSession session = sqlSessionFactory.openSession()) {17 UserMapper mapper = session.getMapper(UserMapper.class);18 19 // 第一次查询20 User user = mapper.getUserById(id);21 // 处理用户数据...22 23 // 第二次查询利用一级缓存24 User sameUser = mapper.getUserById(id);25 // 继续处理...26 27 session.commit();28 }29 }30}6.2 二级缓存配置
二级缓存是Mapper级别的缓存,可以跨越多个SqlSession共享,需要手动启用。
- 基本配置
- 缓存控制
- 淘汰策略
- 注解配置
启用二级缓存的步骤:
- 全局配置文件中启用缓存
1<settings>2 <setting name="cacheEnabled" value="true"/> <!-- 默认就是true -->3</settings>- 在Mapper XML中配置缓存
1<mapper namespace="com.example.mapper.UserMapper">2 <!-- 启用该命名空间的二级缓存 -->3 <cache 4 eviction="LRU" <!-- 缓存淘汰策略:LRU/FIFO/SOFT/WEAK -->5 flushInterval="60000" <!-- 刷新间隔(毫秒),不设置则不自动刷新 -->6 size="512" <!-- 缓存对象数量上限 -->7 readOnly="false"/> <!-- false允许修改对象,但需要可序列化 -->8 9 <!-- 映射语句... -->10</mapper>- 确保实体类实现Serializable接口
1public class User implements Serializable {2 private static final long serialVersionUID = 1L;3 4 private Long id;5 private String username;6 // 其他属性...7}控制单个语句的缓存行为:
1<!-- 使用缓存(默认值为true) -->2<select id="getUserById" resultType="User" useCache="true">3 SELECT * FROM user WHERE id = #{id}4</select>56<!-- 禁用缓存 -->7<select id="getUserForUpdate" resultType="User" useCache="false">8 SELECT * FROM user WHERE id = #{id}9</select>1011<!-- 清除缓存(默认对修改操作为true,查询为false) -->12<select id="getUserByEmail" resultType="User" flushCache="true">13 SELECT * FROM user WHERE email = #{email}14</select>1516<update id="updateUser" flushCache="true">17 UPDATE user SET username = #{username} WHERE id = #{id}18</update>二级缓存的作用范围是namespace级别的,如果要在多个Mapper之间共享缓存,可以使用<cache-ref>:
1<mapper namespace="com.example.mapper.OrderMapper">2 <!-- 引用UserMapper的缓存配置 -->3 <cache-ref namespace="com.example.mapper.UserMapper"/>4 5 <!-- 映射语句... -->6</mapper>MyBatis二级缓存支持四种淘汰策略:
- LRU (Least Recently Used):最近最少使用,移除最长时间未使用的对象,默认值
- FIFO (First In First Out):先进先出,按对象进入缓存的顺序移除
- SOFT:软引用,基于垃圾回收器的状态和软引用规则移除对象
- WEAK:弱引用,更积极地基于垃圾回收器和弱引用规则移除对象
1<!-- LRU淘汰策略 -->2<cache eviction="LRU" size="1024"/>34<!-- FIFO淘汰策略 -->5<cache eviction="FIFO" size="1024"/>67<!-- 基于软引用 - 内存不足时回收 -->8<cache eviction="SOFT"/>910<!-- 基于弱引用 - 下次GC时立即回收 -->11<cache eviction="WEAK"/>使用注解方式配置二级缓存:
1@CacheNamespace(2 eviction = FifoCache.class,3 flushInterval = 60000,4 size = 512,5 readWrite = false6)7public interface UserMapper {8 9 @Select("SELECT * FROM user WHERE id = #{id}")10 @Options(useCache = true)11 User getUserById(Long id);12 13 @Update("UPDATE user SET username = #{username} WHERE id = #{id}")14 @Options(flushCache = Options.FlushCachePolicy.TRUE)15 int updateUser(User user);16}1718// 缓存引用19@CacheNamespaceRef(UserMapper.class)20public interface OrderMapper {21 // ...22}6.3 自定义缓存实现
MyBatis支持自定义缓存实现,可以集成第三方缓存,如Redis、Ehcache等。
- 自定义缓存
- Redis缓存集成
- Spring集成
自定义缓存需要实现MyBatis的Cache接口:
1package com.example.cache;23import org.apache.ibatis.cache.Cache;45public class CustomCache implements Cache {6 private final String id;7 private final Map<Object, Object> cache = new ConcurrentHashMap<>();8 9 public CustomCache(String id) {10 this.id = id;11 }12 13 @Override14 public String getId() {15 return id;16 }17 18 @Override19 public void putObject(Object key, Object value) {20 cache.put(key, value);21 }22 23 @Override24 public Object getObject(Object key) {25 return cache.get(key);26 }27 28 @Override29 public Object removeObject(Object key) {30 return cache.remove(key);31 }32 33 @Override34 public void clear() {35 cache.clear();36 }37 38 @Override39 public int getSize() {40 return cache.size();41 }42}在Mapper中使用自定义缓存:
1<mapper namespace="com.example.mapper.UserMapper">2 <cache type="com.example.cache.CustomCache">3 <property name="cacheType" value="LRU"/>4 <property name="maxSize" value="1000"/>5 </cache>6 7 <!-- 映射语句... -->8</mapper>集成Redis作为MyBatis的二级缓存:
- 添加依赖
1<dependency>2 <groupId>org.mybatis.caches</groupId>3 <artifactId>mybatis-redis</artifactId>4 <version>1.0.0-beta2</version>5</dependency>- 配置Redis连接属性
1redis.host=localhost2redis.port=63793redis.connectionTimeout=50004redis.password=5redis.database=0- 在Mapper中使用Redis缓存
1<mapper namespace="com.example.mapper.UserMapper">2 <cache type="org.mybatis.caches.redis.RedisCache">3 <property name="timeToLive" value="3600000"/> <!-- 缓存过期时间(毫秒) -->4 <property name="configurationPropertiesFile" value="redis.properties"/>5 </cache>6 7 <!-- 映射语句... -->8</mapper>- 自定义Redis缓存实现
1public class CustomRedisCache implements Cache {2 private final String id;3 private static JedisPool jedisPool;4 5 // 初始化Redis连接池6 static {7 JedisPoolConfig config = new JedisPoolConfig();8 config.setMaxTotal(100);9 config.setMaxIdle(20);10 jedisPool = new JedisPool(config, "localhost", 6379);11 }12 13 public CustomRedisCache(String id) {14 this.id = id;15 }16 17 @Override18 public String getId() {19 return this.id;20 }21 22 @Override23 public void putObject(Object key, Object value) {24 try (Jedis jedis = jedisPool.getResource()) {25 jedis.set(serializeKey(key), SerializeUtil.serialize(value));26 jedis.expire(serializeKey(key), 3600); // 1小时过期27 }28 }29 30 @Override31 public Object getObject(Object key) {32 try (Jedis jedis = jedisPool.getResource()) {33 byte[] value = jedis.get(serializeKey(key));34 return value != null ? SerializeUtil.unserialize(value) : null;35 }36 }37 38 @Override39 public Object removeObject(Object key) {40 try (Jedis jedis = jedisPool.getResource()) {41 return jedis.del(serializeKey(key));42 }43 }44 45 @Override46 public void clear() {47 try (Jedis jedis = jedisPool.getResource()) {48 jedis.flushDB();49 }50 }51 52 @Override53 public int getSize() {54 try (Jedis jedis = jedisPool.getResource()) {55 return jedis.dbSize().intValue();56 }57 }58 59 private byte[] serializeKey(Object key) {60 if (key instanceof String) {61 return (this.id + ":" + key).getBytes();62 }63 return (this.id + ":" + key.hashCode()).getBytes();64 }65}在Spring环境中使用MyBatis缓存:
1@Configuration2@MapperScan("com.example.mapper")3public class MyBatisConfig {4 5 @Bean6 public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {7 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();8 factoryBean.setDataSource(dataSource);9 10 // 设置MyBatis配置11 org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();12 configuration.setCacheEnabled(true); // 启用二级缓存13 configuration.setLocalCacheScope(LocalCacheScope.SESSION); // 设置一级缓存作用域14 factoryBean.setConfiguration(configuration);15 16 return factoryBean.getObject();17 }18 19 // 配置事务管理器20 @Bean21 public DataSourceTransactionManager transactionManager(DataSource dataSource) {22 return new DataSourceTransactionManager(dataSource);23 }24}2526// Service层配置27@Service28@Transactional29public class UserService {30 @Autowired31 private UserMapper userMapper;32 33 // 在事务中,同一个SqlSession会被重用34 // 一级缓存在整个事务中有效35 public User getUserById(Long id) {36 return userMapper.getUserById(id);37 }38}6.4 性能优化策略
除了缓存,MyBatis还有多种性能优化策略,可以显著提高系统性能。
- 通用策略
- 延迟加载
- 分页优化
- 批量处理
常见的MyBatis性能优化策略:
-
SQL优化
- 仅查询必要的列,避免SELECT *
- 合理使用索引
- 优化JOIN操作和子查询
- 分页查询大数据集
-
参数处理
- 使用批量操作减少数据库交互
- 合理设置PreparedStatement的参数
- 对于IN条件,限制参数数量
-
结果映射
- 使用ResultMap避免重复定义列映射
- 合理配置延迟加载
- 避免复杂的嵌套查询
-
配置优化
- 使用连接池管理数据库连接
- 优化日志配置,生产环境关闭详细日志
- 适当设置缓存大小和刷新间隔
1<!-- 避免SELECT * -->2<select id="getUsers" resultType="User">3 SELECT id, username, email FROM user4 WHERE status = #{status}5 LIMIT #{offset}, #{limit}6</select>78<!-- 使用批量操作 -->9<insert id="batchInsert" parameterType="list">10 INSERT INTO user (username, email, status)11 VALUES12 <foreach collection="list" item="user" separator=",">13 (#{user.username}, #{user.email}, #{user.status})14 </foreach>15</insert>MyBatis的延迟加载可以显著提高性能,特别是在处理复杂对象关系时:
1<!-- 全局配置 -->2<settings>3 <setting name="lazyLoadingEnabled" value="true"/>4 <setting name="aggressiveLazyLoading" value="false"/>5 <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode"/>6</settings>78<!-- 特定关联的延迟加载配置 -->9<resultMap id="UserMap" type="User">10 <id property="id" column="id"/>11 <result property="username" column="username"/>12 13 <!-- 延迟加载用户订单 -->14 <collection property="orders" 15 select="getOrdersByUserId" 16 column="id" 17 fetchType="lazy"/>18</resultMap>1920<!-- 覆盖全局配置,不使用延迟加载 -->21<resultMap id="UserProfileMap" type="User">22 <id property="id" column="id"/>23 <association property="profile" 24 select="getProfileByUserId" 25 column="id" 26 fetchType="eager"/>27</resultMap>处理大数据集时,分页查询是重要的优化手段:
- 使用MyBatis内置的RowBounds(内存分页)
1// 不建议用于大数据量场景2public List<User> getUsersByPage(int offset, int limit) {3 return sqlSession.selectList("getUserList", null, new RowBounds(offset, limit));4}- 使用物理分页(推荐)
1<select id="getUsersByPage" resultType="User">2 SELECT * FROM user3 ORDER BY id4 LIMIT #{offset}, #{limit}5</select>- 使用PageHelper插件(推荐)
1<dependency>2 <groupId>com.github.pagehelper</groupId>3 <artifactId>pagehelper</artifactId>4 <version>5.2.0</version>5</dependency>1// 在查询前设置分页参数2PageHelper.startPage(pageNum, pageSize);3List<User> users = userMapper.getAllUsers();45// 获取分页信息6PageInfo<User> pageInfo = new PageInfo<>(users);批量操作可以显著提高大量数据处理的性能:
- 使用foreach进行批量插入
1<insert id="batchInsert" parameterType="list">2 INSERT INTO user (username, email, status)3 VALUES4 <foreach collection="list" item="user" separator=",">5 (#{user.username}, #{user.email}, #{user.status})6 </foreach>7</insert>- 使用JDBC批处理
1@Update({"<script>",2 "UPDATE user",3 "<set>",4 "status = #{status}",5 "</set>",6 "WHERE id IN",7 "<foreach collection='ids' item='id' open='(' separator=',' close=')'>",8 "#{id}",9 "</foreach>",10 "</script>"})11@Options(useGeneratedKeys = false, flushCache = Options.FlushCachePolicy.TRUE, 12 statementType = StatementType.PREPARED)13void batchUpdateUserStatus(@Param("ids") List<Long> ids, @Param("status") int status);1415// 在Spring配置中启用批处理16@Bean17public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {18 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();19 factoryBean.setDataSource(dataSource);20 21 // 先构建SqlSessionFactory22 SqlSessionFactory factory = factoryBean.getObject();23 // 再配置ExecutorType24 factory.getConfiguration().setDefaultExecutorType(ExecutorType.BATCH);25 26 return factory;27}2829// 使用批处理执行器30public void batchUpdateUsers(List<User> users) {31 try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {32 UserMapper mapper = sqlSession.getMapper(UserMapper.class);33 34 for (User user : users) {35 mapper.updateUser(user);36 }37 38 // 批量提交39 sqlSession.flushStatements();40 sqlSession.commit();41 }42}- 合理使用缓存:缓存不是万能的,需要考虑数据一致性
- 避免过度优化:先找出真正的性能瓶颈,再有针对性地优化
- 监控与测试:引入性能监控,验证优化效果
- 内存消耗:缓存会增加内存使用,需要平衡配置
- 定期维护:清理不必要的缓存,执行必要的数据库优化
- 使用二级缓存:为频繁查询且很少更新的数据配置缓存
- 延迟加载:只在需要时加载复杂关联对象
- 批量操作:减少数据库往返次数
- 分页查询:避免一次性加载大量数据
- SQL优化:编写高效SQL,善用索引
- 结果集处理:只查询必要的字段
- 连接池配置:优化数据库连接池参数
- 避免N+1问题:使用关联查询替代多次单独查询
7. 插件开发与定制扩展
MyBatis提供了强大的插件机制,可以在SQL执行的关键节点进行拦截并添加自定义行为。插件机制是MyBatis灵活性和可扩展性的重要体现。
7.1 插件机制原理
MyBatis插件基于动态代理和责任链模式设计,通过拦截核心组件的方法调用实现功能扩展。
- 拦截点
- 代理机制
- 插件注解
- 插件加载
MyBatis提供了四个拦截点,对应核心组件的不同处理阶段:
-
Executor:负责执行SQL的核心组件,包括创建Statement、参数设置、执行SQL、处理结果等
update: 执行插入/更新/删除操作query: 执行查询操作flushStatements: 刷新语句commit: 提交事务rollback: 回滚事务getTransaction: 获取事务close: 关闭executorisClosed: 检查executor是否关闭
-
StatementHandler:负责处理JDBC Statement的组件
prepare: 准备Statementparameterize: 设置参数batch: 批处理update: 执行更新query: 执行查询
-
ParameterHandler:负责处理SQL参数的组件
getParameterObject: 获取参数对象setParameters: 设置参数
-
ResultSetHandler:负责处理查询结果集的组件
handleResultSets: 处理结果集handleOutputParameters: 处理输出参数
1@Intercepts({2 // 拦截Executor的update方法3 @Signature(4 type = Executor.class,5 method = "update",6 args = {MappedStatement.class, Object.class}7 ),8 // 拦截Executor的query方法9 @Signature(10 type = Executor.class,11 method = "query",12 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}13 ),14 // 拦截StatementHandler的prepare方法15 @Signature(16 type = StatementHandler.class,17 method = "prepare",18 args = {Connection.class, Integer.class}19 )20})21public class ExamplePlugin implements Interceptor {22 // 实现拦截逻辑...23}MyBatis插件使用JDK动态代理实现方法拦截:
- 插件注册:在MyBatis初始化时,通过
InterceptorChain注册所有插件 - 创建代理:为每个目标对象创建代理,代理会拦截特定方法调用
- 责任链执行:按照插件注册顺序依次执行拦截器链中的插件
- 参数访问:通过
Invocation对象访问被拦截方法的参数和目标对象 - 控制流程:插件可以修改参数、执行额外逻辑、修改返回结果,甚至不执行原方法
1// 初始化插件链2InterceptorChain interceptorChain = new InterceptorChain();3interceptorChain.addInterceptor(new LoggingPlugin());4interceptorChain.addInterceptor(new PaginationPlugin());5interceptorChain.addInterceptor(new PerformancePlugin());67// 创建代理对象8Executor target = new SimpleExecutor();9Executor proxy = (Executor) interceptorChain.pluginAll(target);1011// 执行方法时,会触发插件链12// 3. PerformancePlugin.intercept()13// 2. PaginationPlugin.intercept()14// 1. LoggingPlugin.intercept()15// target.query() // 原方法执行16// 1. 返回结果17// 2. 返回结果18// 3. 返回结果19proxy.query(...);@Intercepts和@Signature注解用于声明拦截点:
1@Documented2@Retention(RetentionPolicy.RUNTIME)3@Target(ElementType.TYPE)4public @interface Intercepts {5 Signature[] value();6}78@Documented9@Retention(RetentionPolicy.RUNTIME)10@Target({})11public @interface Signature {12 Class<?> type(); // 拦截的接口类型13 String method(); // 拦截的方法名14 Class<?>[] args(); // 拦截的方法参数类型15}拦截点的完整匹配规则:
- 被拦截对象必须是
@Signature.type指定的类型 - 被拦截方法必须是
@Signature.method指定的方法名 - 被拦截方法的参数必须与
@Signature.args指定的参数类型列表一致
MyBatis加载和初始化插件的过程:
- 配置解析:解析
mybatis-config.xml中的plugins节点 - 实例化插件:根据配置创建插件实例,并设置属性
- 注册插件:将插件添加到
InterceptorChain中 - 包装目标对象:在创建核心组件时,通过
InterceptorChain.pluginAll()方法包装目标对象
1<plugins>2 <plugin interceptor="com.example.plugin.LoggingPlugin">3 <property name="level" value="DEBUG"/>4 </plugin>5 <plugin interceptor="com.example.plugin.PaginationPlugin">6 <property name="dialect" value="mysql"/>7 </plugin>8</plugins>1// 1. 配置解析2List<InterceptorConfig> interceptorConfigs = parseConfig(root.evalNode("plugins"));34// 2. 实例化插件5for (InterceptorConfig config : interceptorConfigs) {6 Interceptor interceptor = (Interceptor) resolveClass(config.getInterceptorClass()).newInstance();7 interceptor.setProperties(config.getProperties());8 9 // 3. 注册插件10 configuration.addInterceptor(interceptor);11}1213// 4. 包装目标对象(示例:创建Executor)14public Executor newExecutor(Transaction transaction, ExecutorType executorType) {15 Executor executor;16 if (ExecutorType.BATCH == executorType) {17 executor = new BatchExecutor(this, transaction);18 } else if (ExecutorType.REUSE == executorType) {19 executor = new ReuseExecutor(this, transaction);20 } else {21 executor = new SimpleExecutor(this, transaction);22 }23 24 // 包装Executor25 return (Executor) interceptorChain.pluginAll(executor);26}7.2 自定义插件开发
开发MyBatis插件需要实现Interceptor接口,通过拦截点进行功能扩展。以下是常见的插件开发场景。
- 插件基本结构
- SQL日志插件
- 参数加密插件
实现一个MyBatis插件需要完成以下步骤:
- 实现
org.apache.ibatis.plugin.Interceptor接口 - 使用
@Intercepts和@Signature注解定义拦截点 - 在
intercept()方法中实现拦截逻辑 - 在
plugin()方法中包装目标对象 - 实现
setProperties()方法处理插件配置属性
1@Intercepts({2 @Signature(3 type = Executor.class,4 method = "update",5 args = {MappedStatement.class, Object.class}6 )7})8public class ExamplePlugin implements Interceptor {9 10 private Properties properties = new Properties();11 12 @Override13 public Object intercept(Invocation invocation) throws Throwable {14 // 在方法执行前添加逻辑15 Object target = invocation.getTarget(); // 目标对象16 Method method = invocation.getMethod(); // 被拦截的方法17 Object[] args = invocation.getArgs(); // 方法参数18 19 // 执行原方法20 Object result = invocation.proceed();21 22 // 在方法执行后添加逻辑23 24 return result;25 }26 27 @Override28 public Object plugin(Object target) {29 // 包装目标对象为代理对象30 return Plugin.wrap(target, this);31 }32 33 @Override34 public void setProperties(Properties properties) {35 // 设置插件属性36 this.properties = properties;37 }38}实现一个SQL日志记录插件,记录SQL执行时间和参数:
1@Intercepts({2 @Signature(3 type = StatementHandler.class,4 method = "query",5 args = {Statement.class, ResultHandler.class}6 ),7 @Signature(8 type = StatementHandler.class,9 method = "update",10 args = {Statement.class}11 )12})13public class SqlLoggerPlugin implements Interceptor {14 15 private static final Logger logger = LoggerFactory.getLogger(SqlLoggerPlugin.class);16 private boolean showSql = true;17 private boolean showParams = true;18 private boolean showTime = true;19 20 @Override21 public Object intercept(Invocation invocation) throws Throwable {22 StatementHandler handler = (StatementHandler) invocation.getTarget();23 long startTime = System.currentTimeMillis();24 25 try {26 // 执行SQL27 Object result = invocation.proceed();28 29 // 记录日志30 if (showSql || showParams || showTime) {31 BoundSql boundSql = handler.getBoundSql();32 String sql = boundSql.getSql().replaceAll("\\s+", " ");33 Object parameterObject = boundSql.getParameterObject();34 long elapsed = System.currentTimeMillis() - startTime;35 36 StringBuilder logBuilder = new StringBuilder();37 if (showSql) {38 logBuilder.append("SQL: ").append(sql);39 }40 41 if (showParams) {42 logBuilder.append("\nParameters: ").append(parameterObject);43 }44 45 if (showTime) {46 logBuilder.append("\nExecution Time: ").append(elapsed).append("ms");47 }48 49 logger.debug(logBuilder.toString());50 }51 52 return result;53 } catch (Throwable e) {54 long elapsed = System.currentTimeMillis() - startTime;55 logger.error("SQL execution error, Time: {}ms", elapsed, e);56 throw e;57 }58 }59 60 @Override61 public Object plugin(Object target) {62 return Plugin.wrap(target, this);63 }64 65 @Override66 public void setProperties(Properties properties) {67 this.showSql = Boolean.parseBoolean(properties.getProperty("showSql", "true"));68 this.showParams = Boolean.parseBoolean(properties.getProperty("showParams", "true"));69 this.showTime = Boolean.parseBoolean(properties.getProperty("showTime", "true"));70 }71}配置SQL日志插件:
1<plugin interceptor="com.example.plugin.SqlLoggerPlugin">2 <property name="showSql" value="true"/>3 <property name="showParams" value="true"/>4 <property name="showTime" value="true"/>5</plugin>实现一个参数加密插件,自动对指定参数进行加密:
1@Intercepts({2 @Signature(3 type = ParameterHandler.class,4 method = "setParameters",5 args = {PreparedStatement.class}6 )7})8public class ParameterEncryptPlugin implements Interceptor {9 10 private Set<String> encryptFields = new HashSet<>();11 private EncryptionService encryptionService;12 13 @Override14 public Object intercept(Invocation invocation) throws Throwable {15 ParameterHandler handler = (ParameterHandler) invocation.getTarget();16 17 // 获取参数对象18 Field parameterObjectField = handler.getClass().getDeclaredField("parameterObject");19 parameterObjectField.setAccessible(true);20 Object parameterObject = parameterObjectField.get(handler);21 22 // 加密参数23 if (parameterObject != null) {24 if (parameterObject instanceof Map) {25 // 处理Map参数26 Map<String, Object> paramMap = (Map<String, Object>) parameterObject;27 for (String field : encryptFields) {28 if (paramMap.containsKey(field) && paramMap.get(field) != null) {29 String value = paramMap.get(field).toString();30 paramMap.put(field, encryptionService.encrypt(value));31 }32 }33 } else {34 // 处理JavaBean参数35 for (String field : encryptFields) {36 try {37 PropertyDescriptor descriptor = new PropertyDescriptor(field, parameterObject.getClass());38 Method getter = descriptor.getReadMethod();39 Method setter = descriptor.getWriteMethod();40 41 if (getter != null && setter != null) {42 Object value = getter.invoke(parameterObject);43 if (value != null && value instanceof String) {44 setter.invoke(parameterObject, encryptionService.encrypt((String) value));45 }46 }47 } catch (Exception e) {48 // 忽略不存在的属性49 }50 }51 }52 }53 54 // 执行原方法55 return invocation.proceed();56 }57 58 @Override59 public Object plugin(Object target) {60 return Plugin.wrap(target, this);61 }62 63 @Override64 public void setProperties(Properties properties) {65 String fields = properties.getProperty("encryptFields", "");66 for (String field : fields.split(",")) {67 field = field.trim();68 if (!field.isEmpty()) {69 encryptFields.add(field);70 }71 }72 73 // 实际项目中应使用依赖注入74 encryptionService = new AESEncryptionService();75 }76}7778// 加密服务接口和实现79interface EncryptionService {80 String encrypt(String data);81 String decrypt(String encryptedData);82}8384class AESEncryptionService implements EncryptionService {85 private static final String KEY = "1234567890abcdef";86 87 @Override88 public String encrypt(String data) {89 // AES加密实现...90 return "ENCRYPTED:" + data;91 }92 93 @Override94 public String decrypt(String encryptedData) {95 // AES解密实现...96 return encryptedData.substring("ENCRYPTED:".length());97 }98}配置参数加密插件:
1<plugin interceptor="com.example.plugin.ParameterEncryptPlugin">2 <property name="encryptFields" value="password,creditCard,idNumber"/>3</plugin>7.3 分页插件实现
分页是应用中常见的需求,通过插件机制可以实现统一的分页功能,避免手动编写分页代码。
- 自定义分页插件
- PageHelper插件
实现一个基础的分页插件:
1@Intercepts({2 @Signature(3 type = Executor.class,4 method = "query",5 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}6 )7})8public class PaginationPlugin implements Interceptor {9 10 private String databaseType;11 12 @Override13 public Object intercept(Invocation invocation) throws Throwable {14 Object[] args = invocation.getArgs();15 MappedStatement ms = (MappedStatement) args[0];16 Object parameterObject = args[1];17 RowBounds rowBounds = (RowBounds) args[2];18 19 // 判断是否需要分页20 if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {21 // 不需要分页,执行原方法22 return invocation.proceed();23 }24 25 // 获取原始SQL26 BoundSql boundSql = ms.getBoundSql(parameterObject);27 String sql = boundSql.getSql().trim();28 29 // 构建分页SQL30 String pageSql = buildPageSql(sql, rowBounds);31 32 // 创建新的BoundSql33 BoundSql newBoundSql = copyBoundSql(ms, boundSql, pageSql);34 35 // 创建新的MappedStatement36 MappedStatement newMs = copyMappedStatement(ms, newBoundSql);37 38 // 清除RowBounds,避免MyBatis内存分页39 args[0] = newMs;40 args[2] = RowBounds.DEFAULT;41 42 // 执行分页查询43 return invocation.proceed();44 }45 46 /**47 * 根据数据库类型构建分页SQL48 */49 private String buildPageSql(String sql, RowBounds rowBounds) {50 int offset = rowBounds.getOffset();51 int limit = rowBounds.getLimit();52 53 StringBuilder pageSql = new StringBuilder(sql.length() + 100);54 55 if ("mysql".equalsIgnoreCase(databaseType)) {56 pageSql.append(sql)57 .append(" LIMIT ")58 .append(offset)59 .append(", ")60 .append(limit);61 } else if ("oracle".equalsIgnoreCase(databaseType)) {62 pageSql.append("SELECT * FROM ( ")63 .append("SELECT TMP.*, ROWNUM ROW_ID FROM ( ")64 .append(sql)65 .append(" ) TMP WHERE ROWNUM <= ")66 .append(offset + limit)67 .append(" ) WHERE ROW_ID > ")68 .append(offset);69 } else if ("sqlserver".equalsIgnoreCase(databaseType)) {70 // SQL Server 2012+71 pageSql.append(sql)72 .append(" OFFSET ")73 .append(offset)74 .append(" ROWS FETCH NEXT ")75 .append(limit)76 .append(" ROWS ONLY");77 } else {78 // 默认使用MySQL语法79 pageSql.append(sql)80 .append(" LIMIT ")81 .append(offset)82 .append(", ")83 .append(limit);84 }85 86 return pageSql.toString();87 }88 89 /**90 * 创建新的BoundSql91 */92 private BoundSql copyBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {93 // 复制BoundSql的实现...94 // 实际代码需要复制boundSql的所有属性和参数映射95 96 return new BoundSql(97 ms.getConfiguration(),98 sql,99 boundSql.getParameterMappings(),100 boundSql.getParameterObject()101 );102 }103 104 /**105 * 创建新的MappedStatement106 */107 private MappedStatement copyMappedStatement(MappedStatement ms, BoundSql boundSql) {108 // 复制MappedStatement的实现...109 // 实际代码需要使用MappedStatement.Builder来构建新的MappedStatement110 111 return new MappedStatement.Builder(112 ms.getConfiguration(),113 ms.getId(),114 new StaticSqlSource(ms.getConfiguration(), boundSql.getSql(), boundSql.getParameterMappings()),115 ms.getSqlCommandType()116 ).cache(ms.getCache())117 .fetchSize(ms.getFetchSize())118 .flushCacheRequired(ms.isFlushCacheRequired())119 .useCache(ms.isUseCache())120 .resultMaps(ms.getResultMaps())121 .resultSetType(ms.getResultSetType())122 .statementType(ms.getStatementType())123 .timeout(ms.getTimeout())124 .parameterMap(ms.getParameterMap())125 .build();126 }127 128 @Override129 public Object plugin(Object target) {130 return Plugin.wrap(target, this);131 }132 133 @Override134 public void setProperties(Properties properties) {135 this.databaseType = properties.getProperty("databaseType", "mysql");136 }137}配置分页插件:
1<plugin interceptor="com.example.plugin.PaginationPlugin">2 <property name="databaseType" value="mysql"/>3</plugin>使用分页插件:
1// 创建分页参数(offset, limit)2RowBounds rowBounds = new RowBounds(20, 10); // 从第21条开始,获取10条34// 执行分页查询5List<User> users = sqlSession.selectList("getUserList", parameter, rowBounds);PageHelper是MyBatis最流行的分页插件,提供了更加简便的分页API:
- 添加PageHelper依赖
1<dependency>2 <groupId>com.github.pagehelper</groupId>3 <artifactId>pagehelper</artifactId>4 <version>5.2.0</version>5</dependency>- 配置PageHelper插件
1<plugins>2 <plugin interceptor="com.github.pagehelper.PageInterceptor">3 <property name="helperDialect" value="mysql"/>4 <property name="reasonable" value="true"/>5 <property name="supportMethodsArguments" value="true"/>6 </plugin>7</plugins>或者在Spring Boot中配置:
1@Configuration2public class MyBatisConfig {3 @Bean4 public PageInterceptor pageInterceptor() {5 PageInterceptor pageInterceptor = new PageInterceptor();6 Properties properties = new Properties();7 properties.setProperty("helperDialect", "mysql");8 properties.setProperty("reasonable", "true");9 properties.setProperty("supportMethodsArguments", "true");10 pageInterceptor.setProperties(properties);11 return pageInterceptor;12 }13}- 使用PageHelper进行分页
1// 方式1:使用PageHelper.startPage开始分页2PageHelper.startPage(1, 10); // 第1页,每页10条3List<User> users = userMapper.getAllUsers();45// 获取分页信息6PageInfo<User> pageInfo = new PageInfo<>(users);7System.out.println("总记录数: " + pageInfo.getTotal());8System.out.println("总页数: " + pageInfo.getPages());9System.out.println("当前页: " + pageInfo.getPageNum());10System.out.println("每页记录数: " + pageInfo.getPageSize());1112// 方式2:使用PageHelper.offsetPage指定偏移量和限制数13PageHelper.offsetPage(20, 10); // 偏移20条,限制10条14List<User> users = userMapper.getAllUsers();7.4 性能监控插件
通过插件机制可以实现SQL性能监控和统计功能,帮助识别和优化慢查询。
- SQL性能监控
- P6Spy集成
实现一个监控SQL执行时间的插件:
1@Intercepts({2 @Signature(3 type = Executor.class,4 method = "update",5 args = {MappedStatement.class, Object.class}6 ),7 @Signature(8 type = Executor.class,9 method = "query",10 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}11 )12})13public class SqlPerformancePlugin implements Interceptor {14 15 private static final Logger logger = LoggerFactory.getLogger(SqlPerformancePlugin.class);16 private long slowSqlThreshold = 1000; // 慢查询阈值,单位毫秒17 18 // 记录SQL执行统计信息19 private final ConcurrentHashMap<String, SqlStats> sqlStatsMap = new ConcurrentHashMap<>();20 21 @Override22 public Object intercept(Invocation invocation) throws Throwable {23 MappedStatement ms = (MappedStatement) invocation.getArgs()[0];24 Object parameter = invocation.getArgs()[1];25 26 String sqlId = ms.getId();27 BoundSql boundSql = ms.getBoundSql(parameter);28 String sql = boundSql.getSql();29 30 long startTime = System.currentTimeMillis();31 Object result = null;32 boolean success = false;33 34 try {35 // 执行SQL36 result = invocation.proceed();37 success = true;38 return result;39 } catch (Throwable e) {40 throw e;41 } finally {42 long endTime = System.currentTimeMillis();43 long costTime = endTime - startTime;44 45 // 更新统计信息46 updateStats(sqlId, costTime, success);47 48 // 记录慢查询49 if (costTime > slowSqlThreshold) {50 logger.warn("Slow SQL detected: {} ms, SQL ID: {}, SQL: {}", costTime, sqlId, getSqlWithParams(boundSql));51 }52 }53 }54 55 private void updateStats(String sqlId, long costTime, boolean success) {56 SqlStats stats = sqlStatsMap.computeIfAbsent(sqlId, k -> new SqlStats());57 58 stats.incrementCount();59 stats.addExecutionTime(costTime);60 61 if (costTime > stats.getMaxTime()) {62 stats.setMaxTime(costTime);63 }64 65 if (costTime < stats.getMinTime() || stats.getMinTime() == 0) {66 stats.setMinTime(costTime);67 }68 69 if (!success) {70 stats.incrementErrorCount();71 }72 }73 74 private String getSqlWithParams(BoundSql boundSql) {75 // 获取SQL语句和参数的实现76 // 实际代码需要解析参数映射和参数对象77 78 return boundSql.getSql();79 }80 81 /**82 * 获取SQL统计信息83 */84 public Map<String, SqlStats> getSqlStats() {85 return new HashMap<>(sqlStatsMap);86 }87 88 /**89 * 重置统计信息90 */91 public void resetStats() {92 sqlStatsMap.clear();93 }94 95 @Override96 public Object plugin(Object target) {97 return Plugin.wrap(target, this);98 }99 100 @Override101 public void setProperties(Properties properties) {102 String threshold = properties.getProperty("slowSqlThreshold");103 if (threshold != null) {104 this.slowSqlThreshold = Long.parseLong(threshold);105 }106 }107 108 /**109 * SQL统计信息类110 */111 public static class SqlStats {112 private final AtomicLong count = new AtomicLong(0);113 private final AtomicLong totalTime = new AtomicLong(0);114 private final AtomicLong errorCount = new AtomicLong(0);115 private volatile long minTime = 0;116 private volatile long maxTime = 0;117 118 public void incrementCount() {119 count.incrementAndGet();120 }121 122 public void addExecutionTime(long time) {123 totalTime.addAndGet(time);124 }125 126 public void incrementErrorCount() {127 errorCount.incrementAndGet();128 }129 130 public long getCount() {131 return count.get();132 }133 134 public long getTotalTime() {135 return totalTime.get();136 }137 138 public long getErrorCount() {139 return errorCount.get();140 }141 142 public double getAverageTime() {143 long countVal = count.get();144 return countVal == 0 ? 0 : (double) totalTime.get() / countVal;145 }146 147 public long getMinTime() {148 return minTime;149 }150 151 public void setMinTime(long minTime) {152 this.minTime = minTime;153 }154 155 public long getMaxTime() {156 return maxTime;157 }158 159 public void setMaxTime(long maxTime) {160 this.maxTime = maxTime;161 }162 }163}配置性能监控插件:
1<plugin interceptor="com.example.plugin.SqlPerformancePlugin">2 <property name="slowSqlThreshold" value="1000"/>3</plugin>使用监控插件收集的统计信息:
1@RestController2@RequestMapping("/admin/monitoring")3public class MonitoringController {4 5 @Autowired6 private SqlPerformancePlugin sqlPerformancePlugin;7 8 @GetMapping("/sql-stats")9 public Map<String, SqlStats> getSqlStats() {10 return sqlPerformancePlugin.getSqlStats();11 }12 13 @PostMapping("/reset-stats")14 public ResponseEntity<Void> resetStats() {15 sqlPerformancePlugin.resetStats();16 return ResponseEntity.ok().build();17 }18}P6Spy是一个SQL监控框架,可以与MyBatis结合使用:
- 添加P6Spy依赖
1<dependency>2 <groupId>p6spy</groupId>3 <artifactId>p6spy</artifactId>4 <version>3.9.1</version>5</dependency>- 配置P6Spy数据源
1@Configuration2public class DataSourceConfig {3 4 @Bean5 @Primary6 public DataSource dataSource() {7 // 原始数据源8 HikariDataSource dataSource = new HikariDataSource();9 dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");10 dataSource.setUsername("root");11 dataSource.setPassword("password");12 13 // 包装为P6Spy数据源14 return new P6DataSource(dataSource);15 }16}- 配置P6Spy (spy.properties)
1# 使用自定义的日志格式2appender=com.p6spy.engine.spy.appender.Slf4JLogger3logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat4customLogMessageFormat=%(currentTime)|%(executionTime)|%(category)|%(sqlSingleLine)56# 设置执行时间记录阈值(毫秒)7executionThreshold=100089# 设置日期格式10dateformat=yyyy-MM-dd HH:mm:ss1112# 启用/禁用模块13excludecategories=info,debug,result,resultset1415# 排除特定SQL语句16exclude=SELECT NOW()- 自定义P6Spy监听器
1public class CustomP6SpyListener extends JdbcEventListener {2 3 private static final Logger logger = LoggerFactory.getLogger(CustomP6SpyListener.class);4 private ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();5 6 @Override7 public void onBeforeAnyExecute(StatementInformation statementInformation) {8 startTimeThreadLocal.set(System.currentTimeMillis());9 }10 11 @Override12 public void onAfterAnyExecute(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) {13 Long startTime = startTimeThreadLocal.get();14 if (startTime != null) {15 long executionTime = System.currentTimeMillis() - startTime;16 startTimeThreadLocal.remove();17 18 if (executionTime > 1000) { // 大于1秒的查询19 String sql = statementInformation.getSqlWithValues();20 logger.warn("Slow SQL detected: {} ms, SQL: {}", executionTime, sql);21 }22 }23 }24}- 注册自定义监听器
1@PostConstruct2public void initP6Spy() {3 P6SpyOptions.getActiveInstance().setJdbcEventListenerFactory(() -> new CustomP6SpyListener());4}- 专注单一功能:每个插件只做一件事,便于维护和组合
- 性能考虑:插件会影响SQL执行性能,应尽量减少开销
- 异常处理:确保在异常情况下正确传递异常,不干扰正常流程
- 线程安全:多线程环境下插件需要注意线程安全
- 插件顺序:多个插件同时使用时,注意配置顺序(先配置的后执行)
- 可配置性:提供足够的配置选项,适应不同场景
- 优先使用现成插件:对于常见功能,优先使用成熟的社区插件
8. MyBatis实战应用场景
MyBatis在实际项目中有着广泛的应用,从简单的CRUD操作到复杂的多表查询、存储过程调用、批量处理等场景都能很好地支持。本节将展示MyBatis在各种实战场景中的应用。
8.1 多表复杂查询
在实际业务中,经常需要进行多表关联查询,MyBatis提供了多种方式处理复杂查询。
- 多表JOIN查询
- 子查询
- 嵌套查询
- 复杂条件查询
使用JOIN进行多表关联查询:
1<select id="getOrderDetails" resultMap="OrderDetailMap">2 SELECT 3 o.id AS order_id,4 o.order_no,5 o.create_time,6 o.status,7 o.total_amount,8 u.id AS user_id,9 u.username,10 u.email,11 p.id AS product_id,12 p.name AS product_name,13 p.price,14 oi.quantity,15 oi.subtotal16 FROM orders o17 LEFT JOIN user u ON o.user_id = u.id18 LEFT JOIN order_item oi ON o.id = oi.order_id19 LEFT JOIN product p ON oi.product_id = p.id20 WHERE o.id = #{orderId}21</select>2223<resultMap id="OrderDetailMap" type="OrderDetail">24 <id property="orderId" column="order_id"/>25 <result property="orderNo" column="order_no"/>26 <result property="createTime" column="create_time"/>27 <result property="status" column="status"/>28 <result property="totalAmount" column="total_amount"/>29 30 <association property="user" javaType="User">31 <id property="id" column="user_id"/>32 <result property="username" column="username"/>33 <result property="email" column="email"/>34 </association>35 36 <collection property="items" ofType="OrderItemDetail">37 <id property="productId" column="product_id"/>38 <result property="productName" column="product_name"/>39 <result property="price" column="price"/>40 <result property="quantity" column="quantity"/>41 <result property="subtotal" column="subtotal"/>42 </collection>43</resultMap>使用子查询处理复杂查询需求:
1<select id="getHighValueCustomers" resultType="User">2 SELECT u.* FROM user u3 WHERE u.id IN (4 SELECT DISTINCT o.user_id5 FROM orders o6 WHERE o.total_amount > #{minAmount}7 AND o.create_time BETWEEN #{startDate} AND #{endDate}8 GROUP BY o.user_id9 HAVING SUM(o.total_amount) > #{totalSpending}10 )11 ORDER BY u.created_time DESC12</select>使用嵌套查询分步获取关联数据:
1<!-- 获取订单主信息 -->2<select id="getOrderById" resultMap="OrderMap">3 SELECT id, order_no, user_id, status, total_amount, create_time4 FROM orders5 WHERE id = #{id}6</select>78<resultMap id="OrderMap" type="Order">9 <id property="id" column="id"/>10 <result property="orderNo" column="order_no"/>11 <result property="status" column="status"/>12 <result property="totalAmount" column="total_amount"/>13 <result property="createTime" column="create_time"/>14 15 <!-- 关联用户信息 -->16 <association property="user" column="user_id"17 select="getUserById" fetchType="eager"/>18 19 <!-- 关联订单项 -->20 <collection property="items" column="id"21 select="getOrderItemsByOrderId" fetchType="lazy"/>22</resultMap>2324<!-- 获取用户信息 -->25<select id="getUserById" resultType="User">26 SELECT id, username, email, phone27 FROM user28 WHERE id = #{userId}29</select>3031<!-- 获取订单项 -->32<select id="getOrderItemsByOrderId" resultMap="OrderItemMap">33 SELECT 34 oi.id, oi.order_id, oi.product_id, 35 oi.quantity, oi.price, oi.subtotal,36 p.name as product_name, p.image as product_image37 FROM order_item oi38 LEFT JOIN product p ON oi.product_id = p.id39 WHERE oi.order_id = #{orderId}40</select>4142<resultMap id="OrderItemMap" type="OrderItem">43 <id property="id" column="id"/>44 <result property="orderId" column="order_id"/>45 <result property="productId" column="product_id"/>46 <result property="quantity" column="quantity"/>47 <result property="price" column="price"/>48 <result property="subtotal" column="subtotal"/>49 50 <association property="product" javaType="Product">51 <id property="id" column="product_id"/>52 <result property="name" column="product_name"/>53 <result property="image" column="product_image"/>54 </association>55</resultMap>实现包含多条件、分组、排序、聚合等复杂查询:
1<select id="searchProducts" resultType="ProductVO">2 SELECT 3 p.id, 4 p.name, 5 p.price,6 p.stock,7 c.name as category_name,8 b.name as brand_name,9 ROUND(AVG(r.rating), 1) as avg_rating,10 COUNT(r.id) as review_count11 FROM product p12 LEFT JOIN category c ON p.category_id = c.id13 LEFT JOIN brand b ON p.brand_id = b.id14 LEFT JOIN review r ON p.id = r.product_id15 <where>16 <if test="keyword != null and keyword != ''">17 AND (18 p.name LIKE CONCAT('%', #{keyword}, '%')19 OR p.description LIKE CONCAT('%', #{keyword}, '%')20 )21 </if>22 <if test="categoryIds != null and categoryIds.size() > 0">23 AND p.category_id IN24 <foreach collection="categoryIds" item="catId" open="(" separator="," close=")">25 #{catId}26 </foreach>27 </if>28 <if test="brandIds != null and brandIds.size() > 0">29 AND p.brand_id IN30 <foreach collection="brandIds" item="brandId" open="(" separator="," close=")">31 #{brandId}32 </foreach>33 </if>34 <if test="minPrice != null">35 AND p.price >= #{minPrice}36 </if>37 <if test="maxPrice != null">38 AND p.price <= #{maxPrice}39 </if>40 <if test="inStock != null and inStock == true">41 AND p.stock > 042 </if>43 <if test="minRating != null">44 HAVING AVG(r.rating) >= #{minRating}45 </if>46 </where>47 GROUP BY p.id, p.name, p.price, p.stock, c.name, b.name48 <choose>49 <when test="sortBy == 'price' and sortDir == 'asc'">50 ORDER BY p.price ASC51 </when>52 <when test="sortBy == 'price' and sortDir == 'desc'">53 ORDER BY p.price DESC54 </when>55 <when test="sortBy == 'rating'">56 ORDER BY avg_rating DESC, review_count DESC57 </when>58 <when test="sortBy == 'newest'">59 ORDER BY p.create_time DESC60 </when>61 <otherwise>62 ORDER BY 63 CASE WHEN p.is_featured = 1 THEN 0 ELSE 1 END,64 p.sort_order65 </otherwise>66 </choose>67 LIMIT #{offset}, #{limit}68</select>8.2 存储过程调用
MyBatis支持调用数据库存储过程,为特定场景提供高效的处理能力。
- 基本调用
- 输入输出参数
- 游标结果
- 批处理存储过程
调用不带参数的简单存储过程:
1<select id="callGetSystemDate" statementType="CALLABLE" resultType="Date">2 {call get_system_date()}3</select>调用带输入参数的存储过程:
1<select id="getUserOrders" parameterType="int" statementType="CALLABLE" resultMap="OrderMap">2 {call get_user_orders(#{userId,jdbcType=INTEGER})}3</select>调用带输入输出参数的存储过程:
1<parameterMap id="processOrderParam" type="map">2 <parameter property="orderId" mode="IN" jdbcType="INTEGER"/>3 <parameter property="status" mode="IN" jdbcType="VARCHAR"/>4 <parameter property="result" mode="OUT" jdbcType="INTEGER"/>5 <parameter property="message" mode="OUT" jdbcType="VARCHAR"/>6</parameterMap>78<update id="processOrder" parameterMap="processOrderParam" statementType="CALLABLE">9 {call process_order(?, ?, ?, ?)}10</update>Java代码调用带输出参数的存储过程:
1public String processOrder(Long orderId, String status) {2 Map<String, Object> params = new HashMap<>();3 params.put("orderId", orderId);4 params.put("status", status);5 params.put("result", null); // 输出参数6 params.put("message", null); // 输出参数7 8 orderMapper.processOrder(params);9 10 Integer result = (Integer) params.get("result");11 String message = (String) params.get("message");12 13 if (result == 1) {14 return "处理成功: " + message;15 } else {16 return "处理失败: " + message;17 }18}处理返回游标的存储过程:
1<resultMap id="ProductMap" type="Product">2 <id property="id" column="id"/>3 <result property="name" column="name"/>4 <result property="price" column="price"/>5 <result property="stock" column="stock"/>6</resultMap>78<parameterMap id="categoryProductsParam" type="map">9 <parameter property="categoryId" mode="IN" jdbcType="INTEGER"/>10 <parameter property="productCursor" mode="OUT" jdbcType="CURSOR" resultMap="ProductMap"/>11</parameterMap>1213<select id="getCategoryProducts" parameterMap="categoryProductsParam" statementType="CALLABLE">14 {call get_category_products(?, ?)}15</select>Java代码处理游标结果:
1public List<Product> getCategoryProducts(Integer categoryId) {2 Map<String, Object> params = new HashMap<>();3 params.put("categoryId", categoryId);4 params.put("productCursor", null); // 输出参数-游标5 6 orderMapper.getCategoryProducts(params);7 8 // 处理游标结果9 List<Product> products = new ArrayList<>();10 try (Cursor<Product> cursor = (Cursor<Product>) params.get("productCursor")) {11 cursor.forEach(products::add);12 }13 14 return products;15}使用批处理方式调用存储过程处理大量数据:
1@Transactional2public void batchProcessOrders(List<Order> orders) {3 try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {4 OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);5 6 for (Order order : orders) {7 Map<String, Object> params = new HashMap<>();8 params.put("orderId", order.getId());9 params.put("status", order.getStatus());10 params.put("result", null);11 params.put("message", null);12 13 mapper.processOrder(params);14 }15 16 // 提交批处理17 sqlSession.flushStatements();18 sqlSession.commit();19 }20}8.3 批量数据处理
在处理大量数据时,批量操作可以显著提高性能,减少数据库交互次数。
- 批量插入
- 批量更新
- 批处理执行器
- 批量处理优化
使用foreach实现批量插入:
1<insert id="batchInsertUsers" parameterType="list">2 INSERT INTO user (username, email, password, status, create_time)3 VALUES4 <foreach collection="list" item="user" separator=",">5 (6 #{user.username},7 #{user.email},8 #{user.password},9 #{user.status},10 #{user.createTime}11 )12 </foreach>13</insert>批量更新有两种常见方式:
- 使用
case when语法:
1<update id="batchUpdateUserStatus" parameterType="map">2 UPDATE user3 SET status = 4 <foreach collection="userStatusList" item="item" open="CASE id" close="END">5 WHEN #{item.userId} THEN #{item.status}6 </foreach>7 WHERE id IN8 <foreach collection="userStatusList" item="item" open="(" separator="," close=")">9 #{item.userId}10 </foreach>11</update>- 使用多语句执行:
1<update id="batchUpdateUsers" parameterType="list">2 <foreach collection="list" item="user" separator=";">3 UPDATE user4 <set>5 <if test="user.username != null">username = #{user.username},</if>6 <if test="user.email != null">email = #{user.email},</if>7 <if test="user.status != null">status = #{user.status},</if>8 update_time = #{user.updateTime}9 </set>10 WHERE id = #{user.id}11 </foreach>12</update>注意:多语句执行需要在JDBC连接URL中启用:
1jdbc:mysql://localhost:3306/mydb?allowMultiQueries=true使用MyBatis的批处理执行器:
1@Service2public class UserService {3 4 @Autowired5 private SqlSessionFactory sqlSessionFactory;6 7 @Transactional8 public void batchUpdateUsers(List<User> users) {9 try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {10 UserMapper mapper = sqlSession.getMapper(UserMapper.class);11 12 for (int i = 0; i < users.size(); i++) {13 mapper.updateUser(users.get(i));14 15 // 每500条提交一次,避免批量过大16 if (i > 0 && i % 500 == 0) {17 sqlSession.flushStatements();18 }19 }20 21 // 最后提交剩余的22 sqlSession.flushStatements();23 }24 }25}批量处理的几种优化策略:
- 分批处理大量数据:
1public void processMassiveData(List<User> allUsers) {2 int batchSize = 1000;3 int totalUsers = allUsers.size();4 5 for (int i = 0; i < totalUsers; i += batchSize) {6 int endIndex = Math.min(i + batchSize, totalUsers);7 List<User> userBatch = allUsers.subList(i, endIndex);8 9 // 处理当前批次10 batchUpdateUsers(userBatch);11 12 // 清理资源,避免内存溢出13 System.gc();14 }15}- 使用多线程并行处理:
1public void parallelProcessData(List<User> allUsers) {2 int batchSize = 1000;3 int totalUsers = allUsers.size();4 int processors = Runtime.getRuntime().availableProcessors();5 6 // 创建线程池7 ExecutorService executorService = Executors.newFixedThreadPool(processors);8 CountDownLatch latch = new CountDownLatch(totalUsers / batchSize + (totalUsers % batchSize > 0 ? 1 : 0));9 10 for (int i = 0; i < totalUsers; i += batchSize) {11 int startIndex = i;12 int endIndex = Math.min(i + batchSize, totalUsers);13 14 executorService.submit(() -> {15 try {16 List<User> userBatch = allUsers.subList(startIndex, endIndex);17 batchUpdateUsers(userBatch);18 } finally {19 latch.countDown();20 }21 });22 }23 24 try {25 latch.await(); // 等待所有批次处理完成26 } catch (InterruptedException e) {27 Thread.currentThread().interrupt();28 } finally {29 executorService.shutdown();30 }31}8.4 动态权限SQL
在企业应用中,数据权限控制是一个常见需求,MyBatis的动态SQL特性可以很好地实现基于用户角色和权限的动态查询。
- 部门数据权限
- 权限拦截器
- 角色权限控制
- 字段级权限
实现基于用户部门的数据权限控制:
1<select id="getUserList" resultType="User">2 SELECT u.* FROM sys_user u3 LEFT JOIN sys_dept d ON u.dept_id = d.id4 <where>5 <!-- 根据用户权限类型动态生成SQL -->6 <choose>7 <!-- 全部数据权限 -->8 <when test="user.dataScope == 'ALL'">9 </when>10 11 <!-- 本部门及以下数据权限 -->12 <when test="user.dataScope == 'DEPT_AND_CHILD'">13 AND (d.id = #{user.deptId} OR d.parent_ids LIKE CONCAT('%,', #{user.deptId}, ',%'))14 </when>15 16 <!-- 本部门数据权限 -->17 <when test="user.dataScope == 'DEPT'">18 AND d.id = #{user.deptId}19 </when>20 21 <!-- 仅个人数据权限 -->22 <when test="user.dataScope == 'SELF'">23 AND u.create_by = #{user.userId}24 </when>25 26 <!-- 自定义数据权限 -->27 <when test="user.dataScope == 'CUSTOM' and user.deptIds != null and user.deptIds.size() > 0">28 AND d.id IN29 <foreach collection="user.deptIds" item="deptId" open="(" separator="," close=")">30 #{deptId}31 </foreach>32 </when>33 34 <!-- 默认无权限 -->35 <otherwise>36 AND 1=237 </otherwise>38 </choose>39 40 <!-- 其他查询条件 -->41 <if test="username != null and username != ''">42 AND u.username LIKE CONCAT('%', #{username}, '%')43 </if>44 <if test="status != null">45 AND u.status = #{status}46 </if>47 </where>48 ORDER BY u.create_time DESC49</select>使用MyBatis插件实现自动化的权限控制:
1@Intercepts({2 @Signature(3 type = StatementHandler.class,4 method = "prepare",5 args = {Connection.class, Integer.class}6 )7})8public class DataPermissionInterceptor implements Interceptor {9 10 @Override11 public Object intercept(Invocation invocation) throws Throwable {12 StatementHandler handler = (StatementHandler) invocation.getTarget();13 MetaObject metaObject = SystemMetaObject.forObject(handler);14 15 // 获取SQL信息16 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");17 String id = mappedStatement.getId();18 19 // 判断是否需要进行数据权限控制20 if (!isDataPermissionEnabled(id)) {21 return invocation.proceed();22 }23 24 // 获取当前用户权限信息25 LoginUser loginUser = getLoginUser();26 if (loginUser == null) {27 return invocation.proceed();28 }29 30 // 获取原始SQL31 BoundSql boundSql = handler.getBoundSql();32 String originalSql = boundSql.getSql();33 34 // 构建数据权限SQL35 String permissionSql = buildPermissionSql(originalSql, loginUser);36 37 // 替换原始SQL38 metaObject.setValue("delegate.boundSql.sql", permissionSql);39 40 return invocation.proceed();41 }42 43 /**44 * 构建数据权限SQL45 */46 private String buildPermissionSql(String originalSql, LoginUser loginUser) {47 // 解析SQL,找到适合注入权限条件的位置48 49 // 根据用户权限类型构建不同的权限条件50 StringBuilder permissionBuilder = new StringBuilder();51 52 if ("ALL".equals(loginUser.getDataScope())) {53 // 全部数据权限,不做处理54 return originalSql;55 } else if ("DEPT_AND_CHILD".equals(loginUser.getDataScope())) {56 // 本部门及以下数据权限57 permissionBuilder.append(" AND (d.id = '")58 .append(loginUser.getDeptId())59 .append("' OR d.parent_ids LIKE '%,")60 .append(loginUser.getDeptId())61 .append(",%')");62 } else if ("DEPT".equals(loginUser.getDataScope())) {63 // 本部门数据权限64 permissionBuilder.append(" AND d.id = '")65 .append(loginUser.getDeptId())66 .append("'");67 } else if ("SELF".equals(loginUser.getDataScope())) {68 // 仅个人数据权限69 permissionBuilder.append(" AND u.create_by = '")70 .append(loginUser.getUserId())71 .append("'");72 } else if ("CUSTOM".equals(loginUser.getDataScope()) && 73 loginUser.getDeptIds() != null && !loginUser.getDeptIds().isEmpty()) {74 // 自定义数据权限75 permissionBuilder.append(" AND d.id IN (");76 for (int i = 0; i < loginUser.getDeptIds().size(); i++) {77 if (i > 0) {78 permissionBuilder.append(",");79 }80 permissionBuilder.append("'").append(loginUser.getDeptIds().get(i)).append("'");81 }82 permissionBuilder.append(")");83 } else {84 // 默认无权限85 permissionBuilder.append(" AND 1=2");86 }87 88 // 注入权限条件到SQL89 int whereIndex = originalSql.toUpperCase().indexOf(" WHERE ");90 if (whereIndex > 0) {91 // SQL中已有WHERE子句92 return originalSql.substring(0, whereIndex + 7) + 93 "(" + originalSql.substring(whereIndex + 7) + ")" + 94 permissionBuilder.toString();95 } else {96 // SQL中没有WHERE子句97 int orderByIndex = originalSql.toUpperCase().indexOf(" ORDER BY ");98 if (orderByIndex > 0) {99 return originalSql.substring(0, orderByIndex) + 100 " WHERE " + permissionBuilder.toString().substring(5) + 101 originalSql.substring(orderByIndex);102 } else {103 return originalSql + " WHERE " + permissionBuilder.toString().substring(5);104 }105 }106 }107 108 /**109 * 判断是否启用数据权限110 */111 private boolean isDataPermissionEnabled(String statementId) {112 // 可以通过注解或配置判断是否需要数据权限控制113 return statementId.contains("getUserList") || 114 statementId.contains("getEmployeeList") ||115 statementId.contains("getOrderList");116 }117 118 /**119 * 获取当前登录用户120 */121 private LoginUser getLoginUser() {122 // 从Spring Security或ThreadLocal中获取当前用户信息123 return SecurityUtils.getLoginUser();124 }125 126 @Override127 public Object plugin(Object target) {128 return Plugin.wrap(target, this);129 }130 131 @Override132 public void setProperties(Properties properties) {133 }134}基于用户角色的动态SQL权限控制:
1<select id="getMenuList" resultType="Menu">2 SELECT3 m.id, m.parent_id, m.name, m.path, m.component, m.visible, m.status4 FROM5 sys_menu m6 LEFT JOIN7 sys_role_menu rm ON m.id = rm.menu_id8 <where>9 <!-- 管理员显示所有菜单 -->10 <if test="user.roles.contains('ROLE_ADMIN')">11 AND m.status = 012 </if>13 <!-- 非管理员只显示有权限的菜单 -->14 <if test="!user.roles.contains('ROLE_ADMIN')">15 AND rm.role_id IN16 <foreach collection="user.roleIds" item="roleId" open="(" separator="," close=")">17 #{roleId}18 </foreach>19 AND m.status = 020 </if>21 </where>22 ORDER BY m.parent_id, m.order_num23</select>实现字段级的权限控制:
1<select id="getUserInfo" resultType="map">2 SELECT 3 u.id,4 u.username,5 u.email,6 <!-- 根据权限控制敏感字段显示 -->7 <if test="hasPermission('user:view:phone')">8 u.phone,9 </if>10 <if test="hasPermission('user:view:idcard')">11 u.id_card,12 </if>13 <if test="hasPermission('user:view:salary')">14 u.salary,15 </if>16 u.status,17 u.create_time18 FROM19 sys_user u20 WHERE21 u.id = #{userId}22</select>自定义权限判断方法:
1@Component2public class SecurityFunction {3 4 @Autowired5 private PermissionService permissionService;6 7 /**8 * 判断当前用户是否拥有指定权限9 */10 public boolean hasPermission(String permission) {11 return permissionService.hasPermission(permission);12 }13}1415// 注册自定义方法到MyBatis配置16@Configuration17public class MyBatisConfig {18 19 @Autowired20 private SecurityFunction securityFunction;21 22 @PostConstruct23 public void init() {24 // 将权限判断方法注册为MyBatis的方法引用25 Configuration configuration = sqlSessionFactory.getConfiguration();26 configuration.getTypeHandlerRegistry()27 .setDefaultEnumTypeHandler(EnumOrdinalTypeHandler.class);28 29 // 注册自定义方法30 configuration.getLanguageRegistry()31 .getDefaultDriverConfig()32 .getLanguageDriver()33 .createParameterHandler(null, null, null)34 .setParameter(null, "hasPermission", 35 (key, val) -> securityFunction.hasPermission(String.valueOf(val)));36 }37}9. 总结与最佳实践
经过前面章节的学习,我们已经全面了解了MyBatis的各个方面。在实际项目中,为了更好地使用MyBatis,需要遵循一些最佳实践和规范,以确保代码质量和系统性能。
9.1 MyBatis使用规范
制定合理的MyBatis使用规范可以提高代码质量、可维护性和性能。
- 命名规范
- 代码组织
- SQL编写规范
- 安全规范
良好的命名规范有助于提高代码可读性和可维护性:
-
命名空间(namespace):使用完整的包名+类名,保持与Mapper接口一致
xml1<mapper namespace="com.example.mapper.UserMapper"> -
SQL语句ID:使用有意义的名称,动词+名词形式,与Mapper接口方法名一致
xml1<select id="getUserById">...</select>2<insert id="insertUser">...</insert>3<update id="updateUserStatus">...</update>4<delete id="deleteUserBatch">...</delete> -
参数名称:使用有意义的参数名,与业务实体属性名保持一致
xml1#{userId}, #{userName}, #{startTime}, #{endTime} -
结果映射ID:使用Entity名+Result/Map后缀
xml1<resultMap id="UserResultMap" type="User">...</resultMap>2<resultMap id="OrderDetailMap" type="OrderDetail">...</resultMap> -
SQL片段ID:使用功能描述+Fragment/Column/Where后缀
xml1<sql id="BaseColumnList">...</sql>2<sql id="CommonWhereFragment">...</sql>
良好的代码组织结构有助于项目管理和团队协作:
-
接口与实现分离
- Mapper接口:
src/main/java/com/example/mapper/ - XML映射文件:
src/main/resources/mapper/
- Mapper接口:
-
模块化组织
- 按业务模块组织Mapper文件
- 大型项目可以按子模块进一步拆分
-
公共SQL提取
-
提取公共列名列表:
xml1<sql id="Base_Column_List">id, name, email, phone, status, create_time, update_time</sql> -
提取公共WHERE条件:
xml1<sql id="Common_Where_Clause">2 <where>3 <if test="status != null">AND status = #{status}</if>4 <if test="startDate != null">AND create_time >= #{startDate}</if>5 </where>6</sql> -
在SQL语句中引用:
xml1<select id="selectUsers" resultMap="UserResultMap">2 SELECT <include refid="Base_Column_List" />3 FROM user4 <include refid="Common_Where_Clause" />5</select>
-
-
合理拆分文件
- 单表操作一个Mapper文件
- 复杂查询可以单独创建Mapper文件
- 避免过大的XML文件,建议不超过1000行
优质的SQL编写规范可以提高可读性和性能:
-
SQL格式化
- 关键字大写(SELECT, FROM, WHERE, AND等)
- 合理使用缩进和换行
- 子句分行编写,增强可读性
xml1<select id="getOrderSummary" resultType="OrderSummary">2 SELECT3 o.id,4 o.order_no,5 o.create_time,6 SUM(oi.price * oi.quantity) AS total_amount,7 COUNT(oi.id) AS item_count8 FROM9 orders o10 LEFT JOIN11 order_item oi ON o.id = oi.order_id12 WHERE13 o.status = #{status}14 AND o.create_time BETWEEN #{startDate} AND #{endDate}15 GROUP BY16 o.id, o.order_no, o.create_time17 ORDER BY18 o.create_time DESC19</select> -
SQL优化原则
- 只查询必要的列,避免
SELECT * - 合理使用索引
- 避免在WHERE子句中对字段进行函数操作
- 合理使用JOIN,避免过多表关联
- 适当添加SQL注释,说明复杂查询的目的
- 只查询必要的列,避免
-
动态SQL编写
- 优先使用
<where>而非WHERE 1=1 - 合理使用
<choose>,<when>,<otherwise>处理互斥条件 - 使用
<set>替代手动拼接SET子句和逗号 - 批量操作使用
<foreach>标签
- 优先使用
安全规范可以防止SQL注入和数据泄露:
-
防止SQL注入
- 使用
#{}参数绑定,避免${}拼接(除非必要) - 必须使用
${}时(如表名、列名),进行严格校验或白名单过滤 - 敏感操作添加权限验证
xml1<!-- 安全的写法 -->2<select id="getUserByUsername" resultType="User">3 SELECT * FROM user WHERE username = #{username}4</select>56<!-- 不安全的写法,可能导致SQL注入 -->7<select id="getUserByUsername" resultType="User">8 SELECT * FROM user WHERE username = '${username}'9</select> - 使用
-
敏感数据处理
- 敏感信息(密码、证件号等)不直接返回
- 使用加密/脱敏处理敏感数据
- 日志中避免打印敏感信息
-
权限控制
- 实现数据行级权限
- 字段级别权限控制
- 操作权限验证
9.2 常见问题解决方案
在使用MyBatis过程中,经常会遇到一些常见问题,这里提供一些解决方案。
- #1 问题
- 类型处理
- 性能优化
- Spring集成问题
N+1查询问题是MyBatis中最常见的性能问题之一:
问题描述:当查询一个列表数据,然后遍历列表查询关联数据时,会产生N+1次查询。
1// 产生N+1查询的代码2List<User> users = userMapper.getAllUsers(); // 1次查询3for (User user : users) {4 List<Order> orders = orderMapper.getOrdersByUserId(user.getId()); // N次查询5 user.setOrders(orders);6}解决方案:
-
使用嵌套结果映射(推荐)
xml1<select id="getUsersWithOrders" resultMap="UserWithOrdersMap">2 SELECT u.*, o.* FROM user u LEFT JOIN orders o ON u.id = o.user_id3</select> -
使用延迟加载(适用于关联数据使用率低的场景)
xml1<!-- 配置启用延迟加载 -->2<setting name="lazyLoadingEnabled" value="true"/>3<setting name="aggressiveLazyLoading" value="false"/>45<resultMap id="userMap" type="User">6 <id property="id" column="id"/>7 <result property="name" column="name"/>8 <collection property="orders" select="getOrdersByUserId" column="id" fetchType="lazy"/>9</resultMap> -
使用批量查询
java1// 先获取用户列表2List<User> users = userMapper.getAllUsers();34// 提取所有用户ID5List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());67// 批量查询订单8Map<Long, List<Order>> orderMap = orderMapper.getOrdersByUserIds(userIds).stream()9 .collect(Collectors.groupingBy(Order::getUserId));1011// 设置订单数据12users.forEach(user -> user.setOrders(orderMap.getOrDefault(user.getId(), Collections.emptyList())));
处理Java类型与数据库类型的映射问题:
问题描述:特殊类型(如枚举、JSON、日期时间等)在Java与数据库之间的转换。
解决方案:
-
枚举类型处理
java1// 枚举定义2public enum UserStatus {3 ACTIVE(1, "活跃"),4 INACTIVE(0, "非活跃"),5 LOCKED(-1, "锁定");67 private final int value;8 private final String desc;910 UserStatus(int value, String desc) {11 this.value = value;12 this.desc = desc;13 }1415 public int getValue() {16 return value;17 }1819 public String getDesc() {20 return desc;21 }2223 public static UserStatus fromValue(int value) {24 for (UserStatus status : values()) {25 if (status.value == value) {26 return status;27 }28 }29 throw new IllegalArgumentException("Invalid status value: " + value);30 }31}3233// 自定义TypeHandler34public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {35 @Override36 public void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {37 ps.setInt(i, parameter.getValue());38 }3940 @Override41 public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {42 int value = rs.getInt(columnName);43 return rs.wasNull() ? null : UserStatus.fromValue(value);44 }4546 @Override47 public UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {48 int value = rs.getInt(columnIndex);49 return rs.wasNull() ? null : UserStatus.fromValue(value);50 }5152 @Override53 public UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {54 int value = cs.getInt(columnIndex);55 return cs.wasNull() ? null : UserStatus.fromValue(value);56 }57} -
JSON类型处理
java1public class JsonTypeHandler<T> extends BaseTypeHandler<T> {2 private final Class<T> clazz;3 private static final ObjectMapper MAPPER = new ObjectMapper();45 public JsonTypeHandler(Class<T> clazz) {6 this.clazz = clazz;7 }89 @Override10 public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {11 try {12 ps.setString(i, MAPPER.writeValueAsString(parameter));13 } catch (JsonProcessingException e) {14 throw new SQLException("Error converting object to JSON", e);15 }16 }1718 @Override19 public T getNullableResult(ResultSet rs, String columnName) throws SQLException {20 return parseJSON(rs.getString(columnName));21 }2223 @Override24 public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {25 return parseJSON(rs.getString(columnIndex));26 }2728 @Override29 public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {30 return parseJSON(cs.getString(columnIndex));31 }3233 private T parseJSON(String json) throws SQLException {34 if (json == null) return null;35 try {36 return MAPPER.readValue(json, clazz);37 } catch (IOException e) {38 throw new SQLException("Error parsing JSON", e);39 }40 }41} -
LocalDateTime类型处理
java1public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {2 @Override3 public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException {4 ps.setTimestamp(i, Timestamp.valueOf(parameter));5 }67 @Override8 public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {9 Timestamp timestamp = rs.getTimestamp(columnName);10 return timestamp != null ? timestamp.toLocalDateTime() : null;11 }1213 @Override14 public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {15 Timestamp timestamp = rs.getTimestamp(columnIndex);16 return timestamp != null ? timestamp.toLocalDateTime() : null;17 }1819 @Override20 public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {21 Timestamp timestamp = cs.getTimestamp(columnIndex);22 return timestamp != null ? timestamp.toLocalDateTime() : null;23 }24}
解决MyBatis常见性能问题:
-
一级缓存问题:一级缓存可能导致脏读
解决方案:
- 调整缓存级别为STATEMENT
xml1<setting name="localCacheScope" value="STATEMENT"/>
- 手动清除缓存
java1sqlSession.clearCache();
- 调整缓存级别为STATEMENT
-
二级缓存问题:二级缓存可能导致数据一致性问题
解决方案:
- 合理设置刷新间隔
xml1<cache flushInterval="60000"/>
- 针对写操作频繁的表禁用二级缓存
- 使用自定义缓存(如Redis)替代默认二级缓存
- 合理设置刷新间隔
-
大数据量分页问题:普通LIMIT分页在数据量大时性能下降
解决方案:
- 使用主键分页
xml1<select id="getPagedUsers">2 SELECT * FROM user3 WHERE id > #{lastId}4 ORDER BY id ASC5 LIMIT #{pageSize}6</select>
- 使用覆盖索引
xml1<select id="getPagedUserIds">2 SELECT id FROM user3 WHERE status = #{status}4 ORDER BY create_time DESC5 LIMIT #{offset}, #{limit}6</select>78<select id="getUsersByIds">9 SELECT * FROM user10 WHERE id IN11 <foreach collection="ids" item="id" open="(" close=")" separator=",">12 #{id}13 </foreach>14</select>
- 使用主键分页
-
复杂条件查询优化
解决方案:
- 使用子查询代替多表JOIN
- 合理使用索引
- 查询条件提前过滤,减少JOIN表的数据量
- 使用EXISTS代替IN适用于外表小内表大的情况
解决与Spring集成时的常见问题:
-
事务管理问题
问题:事务未生效或出现异常回滚不完整
解决方案:
- 确保使用正确的事务管理器
java1@Bean2public PlatformTransactionManager transactionManager(DataSource dataSource) {3 return new DataSourceTransactionManager(dataSource);4}
- 设置正确的事务传播特性
java1@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
- 避免自调用导致事务失效
java1// 错误方式 - 事务不会生效2@Service3public class UserService {4 public void createUser() {5 // 代码...6 this.updateRelatedData(); // 调用同类中的@Transactional方法7 }89 @Transactional10 public void updateRelatedData() {11 // 事务不会生效12 }13}1415// 正确方式16@Service17public class UserService {18 @Autowired19 private UserService self; // 注入自身代理2021 public void createUser() {22 // 代码...23 self.updateRelatedData(); // 通过代理调用24 }2526 @Transactional27 public void updateRelatedData() {28 // 事务生效29 }30}
- 确保使用正确的事务管理器
-
多数据源配置问题
解决方案:
java1@Configuration2public class DataSourceConfig {34 @Bean5 @ConfigurationProperties("spring.datasource.primary")6 public DataSourceProperties primaryDataSourceProperties() {7 return new DataSourceProperties();8 }910 @Bean11 @ConfigurationProperties("spring.datasource.secondary")12 public DataSourceProperties secondaryDataSourceProperties() {13 return new DataSourceProperties();14 }1516 @Bean17 @Primary18 public DataSource primaryDataSource() {19 return primaryDataSourceProperties().initializeDataSourceBuilder().build();20 }2122 @Bean23 public DataSource secondaryDataSource() {24 return secondaryDataSourceProperties().initializeDataSourceBuilder().build();25 }2627 @Bean28 @Primary29 public SqlSessionFactory primarySqlSessionFactory() throws Exception {30 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();31 factoryBean.setDataSource(primaryDataSource());32 factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()33 .getResources("classpath:mapper/primary/**/*.xml"));34 return factoryBean.getObject();35 }3637 @Bean38 public SqlSessionFactory secondarySqlSessionFactory() throws Exception {39 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();40 factoryBean.setDataSource(secondaryDataSource());41 factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()42 .getResources("classpath:mapper/secondary/**/*.xml"));43 return factoryBean.getObject();44 }4546 @Bean47 @Primary48 public SqlSessionTemplate primarySqlSessionTemplate() throws Exception {49 return new SqlSessionTemplate(primarySqlSessionFactory());50 }5152 @Bean53 public SqlSessionTemplate secondarySqlSessionTemplate() throws Exception {54 return new SqlSessionTemplate(secondarySqlSessionFactory());55 }56}5758@Configuration59@MapperScan(basePackages = "com.example.mapper.primary", sqlSessionTemplateRef = "primarySqlSessionTemplate")60public class PrimaryDbConfig {61}6263@Configuration64@MapperScan(basePackages = "com.example.mapper.secondary", sqlSessionTemplateRef = "secondarySqlSessionTemplate")65public class SecondaryDbConfig {66}
9.3 MyBatis技术栈扩展
MyBatis生态系统非常丰富,有多种扩展和工具可以简化开发,提高效率。
- MyBatis-Plus
- 通用Mapper
- PageHelper
- 动态数据源
MyBatis-Plus是基于MyBatis的增强工具,简化了开发,提供了更多强大的功能。
主要特性:
-
通用CRUD:无需编写SQL即可实现基本CRUD操作
java1@Mapper2public interface UserMapper extends BaseMapper<User> {3 // 已内置通用CRUD方法4 // insert, deleteById, updateById, selectById, selectList等5}67// 使用示例8userMapper.insert(user);9userMapper.selectById(1);10userMapper.deleteById(1); -
条件构造器:强大的查询条件构造
java1// 查询条件构造2LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();3queryWrapper4 .eq(User::getStatus, 1)5 .like(User::getUsername, "张")6 .between(User::getCreateTime, startDate, endDate)7 .orderByDesc(User::getCreateTime);89List<User> users = userMapper.selectList(queryWrapper); -
分页插件:简化分页查询
java1// 配置2@Bean3public MybatisPlusInterceptor mybatisPlusInterceptor() {4 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();5 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));6 return interceptor;7}89// 使用10Page<User> page = new Page<>(1, 10);11Page<User> resultPage = userMapper.selectPage(page, queryWrapper);1213// 获取分页信息14long total = resultPage.getTotal();15long pages = resultPage.getPages();16List<User> records = resultPage.getRecords(); -
自动填充:自动处理创建时间、更新时间等字段
java1@TableField(fill = FieldFill.INSERT)2private LocalDateTime createTime;34@TableField(fill = FieldFill.INSERT_UPDATE)5private LocalDateTime updateTime;67// 元数据处理器8@Component9public class MyMetaObjectHandler implements MetaObjectHandler {10 @Override11 public void insertFill(MetaObject metaObject) {12 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());13 this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());14 }1516 @Override17 public void updateFill(MetaObject metaObject) {18 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());19 }20} -
代码生成器:快速生成Entity、Mapper、Service、Controller
java1// 代码生成示例2AutoGenerator generator = new AutoGenerator();34// 数据源配置5generator.setDataSource(dataSourceConfig);67// 全局配置8generator.setGlobalConfig(globalConfig);910// 包配置11generator.setPackageInfo(packageConfig);1213// 策略配置14generator.setStrategy(strategyConfig);1516// 执行17generator.execute(); -
乐观锁插件:支持乐观锁机制
java1@Version2private Integer version;34// 配置乐观锁插件5@Bean6public MybatisPlusInterceptor mybatisPlusInterceptor() {7 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();8 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());9 return interceptor;10}
通用Mapper是一个MyBatis的增强工具,提供通用的CRUD操作。
主要特性:
-
基础CRUD:提供通用的CRUD操作
java1@Mapper2public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User> {3 // 已内置通用方法4}56// 使用7userMapper.insert(user);8userMapper.selectByPrimaryKey(1);9userMapper.updateByPrimaryKey(user);10userMapper.delete(user); -
条件查询:Example查询条件
java1Example example = new Example(User.class);2example.createCriteria()3 .andEqualTo("status", 1)4 .andLike("username", "%张%")5 .andBetween("createTime", startDate, endDate);67example.orderBy("createTime").desc();89List<User> users = userMapper.selectByExample(example); -
特殊操作:提供多种特殊操作接口
java1// 组合接口2public interface UserMapper extends3 Mapper<User>, // 基础CRUD4 IdsMapper<User>, // 根据主键批量操作5 ConditionMapper<User>, // 条件查询6 ExampleMapper<User>, // Example查询7 RowBoundsMapper<User> { // 分页查询8}910// 批量操作11List<Integer> ids = Arrays.asList(1, 2, 3);12userMapper.deleteByIds(StringUtils.join(ids, ","));1314// 条件查询15User condition = new User();16condition.setStatus(1);17List<User> users = userMapper.selectByCondition(condition);
PageHelper是MyBatis的分页插件,简化分页查询操作。
主要特性:
-
简单配置
java1@Configuration2public class MybatisConfig {3 @Bean4 public PageInterceptor pageInterceptor() {5 PageInterceptor pageInterceptor = new PageInterceptor();6 Properties properties = new Properties();7 properties.setProperty("reasonable", "true");8 properties.setProperty("helperDialect", "mysql");9 pageInterceptor.setProperties(properties);10 return pageInterceptor;11 }12} -
开始分页
java1// 方法一:使用PageHelper.startPage2PageHelper.startPage(pageNum, pageSize);3List<User> users = userMapper.selectUsers(condition);45// 方法二:使用PageHelper.offsetPage6PageHelper.offsetPage(offset, limit);7List<User> users = userMapper.selectUsers(condition); -
获取分页信息
java1// 方法一:使用PageInfo包装2PageInfo<User> pageInfo = new PageInfo<>(users);34// 获取分页信息5long total = pageInfo.getTotal();6int pages = pageInfo.getPages();7List<User> list = pageInfo.getList();89// 方法二:使用Page对象10Page<User> page = (Page<User>) users;11long total = page.getTotal();12int pages = page.getPages(); -
排序
java1// 分页并排序2PageHelper.startPage(pageNum, pageSize, "create_time desc");34// 或者5PageHelper.startPage(pageNum, pageSize);6PageHelper.orderBy("create_time desc"); -
合理化
java1// 设置reasonable为true,当页数超过总页数时,自动查询最后一页2PageHelper.startPage(pageNum, pageSize, true);
dynamic-datasource是一个支持多数据源动态切换的组件。
主要特性:
-
配置多数据源
yml1spring:2 datasource:3 dynamic:4 primary: master5 strict: false6 datasource:7 master:8 url: jdbc:mysql://localhost:3306/master9 username: root10 password: root11 driver-class-name: com.mysql.cj.jdbc.Driver12 slave_1:13 url: jdbc:mysql://localhost:3307/slave14 username: root15 password: root16 driver-class-name: com.mysql.cj.jdbc.Driver17 slave_2:18 url: jdbc:mysql://localhost:3308/slave19 username: root20 password: root21 driver-class-name: com.mysql.cj.jdbc.Driver -
使用注解切换数据源
java1@DS("slave_1")2@Service3public class UserServiceImpl implements UserService {4 @Autowired5 private UserMapper userMapper;67 @Override8 public List<User> listUsers() {9 return userMapper.selectList(null);10 }1112 @DS("slave_2")13 @Override14 public User getUserById(Long id) {15 return userMapper.selectById(id);16 }1718 @DS("master")19 @Override20 public boolean updateUser(User user) {21 return userMapper.updateById(user) > 0;22 }23} -
分组策略
java1// 配置2@Bean3public DynamicDataSourceProvider dynamicDataSourceProvider() {4 Map<String, DataSourceProperty> dataSourcePropertiesMap = new HashMap<>();5 // 主库6 dataSourcePropertiesMap.put("master", masterDataSourceProperty());7 // 从库组8 dataSourcePropertiesMap.put("slave_1", slave1DataSourceProperty());9 dataSourcePropertiesMap.put("slave_2", slave2DataSourceProperty());10 return new YmlDynamicDataSourceProvider(dataSourcePropertiesMap);11}1213@Bean14public DynamicDataSourceStrategy loadBalanceDynamicDataSourceStrategy() {15 return new LoadBalanceDynamicDataSourceStrategy();16}1718// 使用19@DS("slave") // slave组,会自动在slave_1和slave_2中负载均衡20public List<User> listUsers() {21 return userMapper.selectList(null);22} -
事务支持
java1@DS("master")2@Transactional3public void createUser(User user) {4 userMapper.insert(user);5 // 在同一个事务中,数据源保持一致6 userRoleMapper.insertUserRole(user.getId(), roleId);7}
- 命名规范:保持SQL ID与Mapper接口方法名一致,使用有意义的命名
- 模块化:按业务模块组织代码,提取公共SQL片段
- SQL优化:只查询必要的列,合理使用索引,避免过多JOIN
- 安全防护:使用参数绑定防止SQL注入,敏感数据加密处理
- 性能优化:合理使用缓存,避免N+1查询,批量处理大量数据
- 集成扩展:根据需求选择合适的MyBatis扩展工具
- 面向接口:遵循面向接口编程原则,降低代码耦合度
- 单一职责:每个Mapper接口专注于单一实体的操作
- 测试覆盖:编写单元测试,确保SQL语句正确
- 版本管理:使用数据库版本管理工具(如Flyway、Liquibase)管理数据库变更
10. 面试题精选
MyBatis是Java面试中常见的持久层框架话题,以下是一些常见面试题及其详细解答。
10.1 基础原理面试题
- 核心原理
- 框架对比
Q: MyBatis的核心工作原理是什么?
A: MyBatis的核心工作原理可以概括为:
-
配置解析阶段:
- 读取XML配置文件或注解配置
- 创建Configuration对象,包含所有配置信息
- 解析映射文件,创建MappedStatement对象
-
初始化阶段:
- 创建SqlSessionFactory实例
- 注册所有Mapper接口
-
执行阶段:
- 通过SqlSessionFactory创建SqlSession
- 通过动态代理创建Mapper接口实现类
- 执行SQL,映射结果
这个过程的核心是通过动态代理将接口方法调用转换为SQL执行,再将结果集映射为Java对象。
Q: SqlSession和SqlSessionFactory的区别是什么?
A:
-
SqlSessionFactory:
- 负责创建SqlSession实例
- 线程安全,可供多个线程共享
- 全局唯一,应用级生命周期
- 类似于数据库连接池,是一个重量级对象
-
SqlSession:
- 提供执行SQL的API
- 非线程安全,不能在多线程中共享
- 会话级生命周期,使用后需关闭
- 类似于数据库连接,是一个轻量级对象
Q: MyBatis的工作流程是怎样的?
A: MyBatis完整工作流程如下:
- 读取配置文件,创建SqlSessionFactoryBuilder对象
- SqlSessionFactoryBuilder解析配置,创建SqlSessionFactory对象
- 从SqlSessionFactory获取SqlSession
- 通过SqlSession获取Mapper接口的代理实现
- 通过Mapper接口调用方法
- SqlSession将请求转发给Executor
- Executor通过StatementHandler创建Statement对象
- ParameterHandler设置参数
- Statement执行SQL
- ResultSetHandler处理结果集,转换为Java对象
- 返回结果,关闭SqlSession
Q: MyBatis与Hibernate有什么区别?
A: MyBatis和Hibernate的主要区别:
| 特性 | MyBatis | Hibernate |
|---|---|---|
| 级别 | 半自动ORM | 全自动ORM |
| SQL控制 | 手写SQL,灵活性高 | 自动生成SQL,控制性低 |
| 学习曲线 | 较低,接近JDBC | 较高,概念较多 |
| 性能 | 较高(直接SQL优化) | 较低(自动生成SQL) |
| 开发效率 | 需编写SQL,效率较低 | 无需编写SQL,效率高 |
| 适用场景 | 复杂SQL查询,性能要求高 | 简单CRUD,快速开发 |
| 映射复杂度 | 灵活,支持复杂映射 | 完善,支持复杂关联 |
| 缓存机制 | 简单的一二级缓存 | 复杂的多级缓存 |
Q: MyBatis相比JDBC有什么优势?
A: MyBatis相比JDBC的优势:
- 代码量减少:无需手动管理连接、设置参数、处理结果集
- 参数映射:自动将Java对象映射为SQL参数
- 结果映射:自动将结果集映射为Java对象
- SQL分离:SQL与代码分离,便于维护
- 动态SQL:支持条件判断、循环等动态SQL构建
- 缓存机制:内置一二级缓存,提升性能
- 事务管理:简化事务管理
- 插件机制:支持插件扩展
- 日志支持:详细的日志记录
Q: MyBatis和JPA各有什么优缺点?
A: MyBatis和JPA的优缺点:
MyBatis优点:
- SQL控制灵活,可优化复杂查询
- 学习成本低,接近原生SQL
- 性能可控,便于调优
- 适合复杂业务和报表查询
MyBatis缺点:
- 需要手写SQL,开发效率低
- 数据库移植性较差
- 对象关系映射功能简单
JPA优点:
- 无需编写SQL,提高开发效率
- 数据库无关性好,便于切换数据库
- 丰富的对象关系映射
- 标准化规范,学习一次可用多处
JPA缺点:
- 复杂查询难以优化
- 学习曲线陡峭
- 对数据库特性支持有限
- 性能调优困难
10.2 配置与映射面试题
- 配置问题
- 映射问题
Q: #和$的区别是什么?
A: 这两种符号用于SQL参数传递,但有本质区别:
-
#:
- 参数占位符,使用PreparedStatement
- 会自动添加引号(如字符串)
- 防止SQL注入
- 支持类型转换
- 例:
WHERE id = #{id}→WHERE id = ?
-
$:
- 文本替换,直接拼接SQL
- 不会添加引号
- 存在SQL注入风险
- 不支持类型转换
- 例:
ORDER BY ${column}→ORDER BY column_name
何时使用$:
- 动态表名:
FROM ${tableName} - 动态列名:
ORDER BY ${columnName} - 动态SQL片段:
${sqlFragment}
安全建议:尽量使用#,必须使用$时要进行严格参数校验。
Q: MyBatis的配置文件中有哪些重要元素?
A: MyBatis配置文件的重要元素(按顺序):
- properties:定义可重用的属性
- settings:修改MyBatis行为的重要设置
- typeAliases:为Java类型设置别名
- typeHandlers:类型处理器,处理Java与JDBC类型转换
- objectFactory:创建结果对象的工厂
- plugins:插件拦截器
- environments:环境配置
- environment:特定环境配置
- transactionManager:事务管理器配置
- dataSource:数据源配置
- databaseIdProvider:数据库厂商标识
- mappers:映射器配置
Q: MyBatis的常用设置有哪些?
A: 常用设置:
- cacheEnabled:是否启用缓存(默认true)
- lazyLoadingEnabled:是否开启延迟加载(默认false)
- aggressiveLazyLoading:是否积极加载对象所有属性(默认false)
- multipleResultSetsEnabled:是否允许单一语句返回多结果集(默认true)
- useColumnLabel:使用列标签代替列名(默认true)
- useGeneratedKeys:使用JDBC的getGeneratedKeys(默认false)
- autoMappingBehavior:自动映射结果集(NONE/PARTIAL/FULL)
- defaultExecutorType:默认执行器类型(SIMPLE/REUSE/BATCH)
- mapUnderscoreToCamelCase:下划线转驼峰命名(默认false)
- localCacheScope:本地缓存作用域(SESSION/STATEMENT)
- jdbcTypeForNull:NULL值的JDBC类型(默认OTHER)
- logImpl:指定日志实现(SLF4J/LOG4J/LOG4J2等)
Q: MyBatis映射文件中常用元素有哪些?
A: 常用元素:
- resultMap:结果映射定义
- sql:可重用的SQL片段
- insert:插入语句
- update:更新语句
- delete:删除语句
- select:查询语句
每种元素都有丰富的属性和子元素,例如:
1<select id="getUser" 2 parameterType="int" 3 resultType="User" 4 flushCache="false" 5 useCache="true" 6 timeout="10000" 7 fetchSize="256" 8 statementType="PREPARED">9 SELECT * FROM user WHERE id = #{id}10</select>Q: resultType和resultMap的区别是什么?
A: 两者都用于指定查询结果的映射方式:
-
resultType:
- 指定返回结果的Java类型
- 要求列名和属性名相同(或配置驼峰映射)
- 简单映射的首选方式
- 例:
resultType="com.example.User"
-
resultMap:
- 自定义结果映射规则
- 处理列名和属性名不匹配情况
- 处理复杂关联关系(一对一、一对多等)
- 例:
resultMap="UserResultMap"
Q: MyBatis如何实现一对一和一对多关联查询?
A: MyBatis提供了两种实现关联查询的方式:
-
嵌套结果映射(单次查询):
xml1<!-- 一对一关联 -->2<resultMap id="userWithProfile" type="User">3 <id property="id" column="id"/>4 <result property="username" column="username"/>5 <association property="profile" javaType="Profile">6 <id property="id" column="profile_id"/>7 <result property="address" column="address"/>8 </association>9</resultMap>1011<!-- 一对多关联 -->12<resultMap id="userWithOrders" type="User">13 <id property="id" column="id"/>14 <result property="username" column="username"/>15 <collection property="orders" ofType="Order">16 <id property="id" column="order_id"/>17 <result property="amount" column="amount"/>18 </collection>19</resultMap> -
嵌套查询(多次查询):
xml1<!-- 一对一关联 -->2<resultMap id="userWithProfile" type="User">3 <id property="id" column="id"/>4 <result property="username" column="username"/>5 <association property="profile"6 column="id"7 select="getProfileByUserId"/>8</resultMap>910<!-- 一对多关联 -->11<resultMap id="userWithOrders" type="User">12 <id property="id" column="id"/>13 <result property="username" column="username"/>14 <collection property="orders"15 column="id"16 select="getOrdersByUserId"/>17</resultMap>
10.3 缓存与性能面试题
- 缓存问题
- 性能问题
Q: MyBatis的缓存机制是怎样的?
A: MyBatis有两级缓存机制:
-
一级缓存:
- SqlSession级别的缓存
- 默认开启,无法关闭
- 作用范围:同一SqlSession内
- 生命周期:SqlSession关闭时结束
- 清除条件:
- 执行UPDATE、INSERT、DELETE操作
- 调用clearCache()方法
- 调用close()方法
- 事务提交或回滚操作
-
二级缓存:
- Mapper级别的缓存(namespace)
- 默认关闭,需手动配置
- 作用范围:同一namespace内的所有SqlSession共享
- 生命周期:应用运行期间
- 启用方式:
xml1<!-- 全局配置 -->2<setting name="cacheEnabled" value="true"/>34<!-- Mapper配置 -->5<cache6 eviction="LRU"7 flushInterval="60000"8 size="512"9 readOnly="false"/>
Q: 什么情况下不应使用二级缓存?
A: 以下情况不适合使用二级缓存:
- 写操作频繁的场景
- 多表关联查询(可能导致脏数据)
- 分布式系统(缓存不一致)
- 需要实时数据的场景
- 对数据一致性要求高的业务
- 表之间存在复杂关联关系
在这些情况下,建议使用Redis等外部缓存代替MyBatis内置的二级缓存。
Q: MyBatis的一级缓存为什么会出现脏读?如何避免?
A: 一级缓存可能导致脏读的原因:
- 同一SqlSession内,两次查询之间数据被其他会话修改
- 缓存中的对象被程序修改,但未同步到数据库
避免一级缓存脏读的方法:
- 调整缓存级别为STATEMENT:
xml1<setting name="localCacheScope" value="STATEMENT"/>
- 关键查询前清空缓存:
java1sqlSession.clearCache();
- 避免共享SqlSession
- 敏感查询使用flushCache="true":
xml1<select id="getUser" flushCache="true">2 SELECT * FROM user WHERE id = #{id}3</select>
Q: 如何优化MyBatis的性能?
A: MyBatis性能优化策略:
-
合理使用缓存:
- 配置二级缓存
- 使用自定义缓存如Redis
- 避免频繁刷新缓存
-
SQL优化:
- 只查询必要的列
- 添加合适的索引
- 避免SELECT *
- 分页查询大数据集
-
延迟加载:
xml1<setting name="lazyLoadingEnabled" value="true"/>2<setting name="aggressiveLazyLoading" value="false"/> -
批量操作:
- 使用批处理执行器
java1SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
- 使用批量插入
xml1<insert id="batchInsert">2 INSERT INTO table (col1, col2) VALUES3 <foreach collection="list" item="item" separator=",">4 (#{item.col1}, #{item.col2})5 </foreach>6</insert>
- 使用批处理执行器
-
结果集处理:
- 限制结果集大小
- 使用流式查询处理大结果集
- 合理设置fetchSize
-
避免N+1问题:
- 使用嵌套结果映射代替嵌套查询
- 使用JOIN代替多次单独查询
-
使用插件:
- 分页插件如PageHelper
- 性能监控插件
Q: 什么是MyBatis中的N+1问题?如何解决?
A: N+1问题是指:
查询N条主记录,然后针对每条主记录再查询关联记录,总共产生N+1次查询。
1// N+1问题示例2List<User> users = userMapper.getAllUsers(); // 1次查询3for (User user : users) {4 List<Order> orders = orderMapper.getOrdersByUserId(user.getId()); // N次查询5 user.setOrders(orders);6}解决N+1问题的方法:
-
使用嵌套结果映射(推荐):
xml1<select id="getUsersWithOrders" resultMap="userOrderMap">2 SELECT u.*, o.*3 FROM user u4 LEFT JOIN orders o ON u.id = o.user_id5</select>67<resultMap id="userOrderMap" type="User">8 <id property="id" column="id"/>9 <result property="name" column="name"/>10 <collection property="orders" ofType="Order">11 <id property="id" column="order_id"/>12 <result property="amount" column="amount"/>13 </collection>14</resultMap> -
延迟加载(适用于关联数据使用率低的场景):
xml1<setting name="lazyLoadingEnabled" value="true"/>2<setting name="aggressiveLazyLoading" value="false"/>34<resultMap id="userMap" type="User">5 <id property="id" column="id"/>6 <result property="name" column="name"/>7 <collection property="orders"8 select="getOrdersByUserId"9 column="id"10 fetchType="lazy"/>11</resultMap> -
批量查询:
java1// 先获取用户列表2List<User> users = userMapper.getAllUsers();34// 提取所有用户ID5List<Long> userIds = users.stream()6 .map(User::getId)7 .collect(Collectors.toList());89// 一次查询所有订单10List<Order> allOrders = orderMapper.getOrdersByUserIds(userIds);1112// 按用户ID分组13Map<Long, List<Order>> orderMap = allOrders.stream()14 .collect(Collectors.groupingBy(Order::getUserId));1516// 设置订单到用户对象17users.forEach(user -> user.setOrders(18 orderMap.getOrDefault(user.getId(), Collections.emptyList())19));
10.4 插件与扩展面试题
- 插件机制
- 常用扩展
Q: MyBatis的插件机制是如何工作的?
A: MyBatis插件机制基于拦截器模式和责任链模式:
-
原理:使用JDK动态代理,在四大核心对象的方法调用前后进行拦截
-
拦截点:
- Executor:执行器,负责SQL执行
- ParameterHandler:参数处理器
- ResultSetHandler:结果集处理器
- StatementHandler:语句处理器
-
实现步骤:
- 实现Interceptor接口
- 使用@Intercepts和@Signature注解定义拦截点
- 实现intercept方法处理拦截逻辑
- 在配置文件中注册插件
-
工作流程:
- 在MyBatis初始化时,解析插件配置
- 创建拦截器链InterceptorChain
- 当创建四大核心对象时,调用InterceptorChain.pluginAll()包装对象
- 执行目标方法时,按插件注册顺序逆序执行拦截器
Q: 如何开发一个自定义MyBatis插件?
A: 开发自定义插件的步骤:
- 定义插件类:
1@Intercepts({2 @Signature(3 type = Executor.class,4 method = "update",5 args = {MappedStatement.class, Object.class}6 ),7 @Signature(8 type = Executor.class,9 method = "query",10 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}11 )12})13public class ExamplePlugin implements Interceptor {14 15 private Properties properties;16 17 @Override18 public Object intercept(Invocation invocation) throws Throwable {19 // 执行前处理20 Object target = invocation.getTarget(); // 被代理对象21 Method method = invocation.getMethod(); // 被拦截方法22 Object[] args = invocation.getArgs(); // 方法参数23 24 // 执行原方法25 Object result = invocation.proceed();26 27 // 执行后处理28 29 return result;30 }31 32 @Override33 public Object plugin(Object target) {34 // 包装目标对象35 return Plugin.wrap(target, this);36 }37 38 @Override39 public void setProperties(Properties properties) {40 // 设置插件属性41 this.properties = properties;42 }43}- 注册插件:
1<plugins>2 <plugin interceptor="com.example.plugin.ExamplePlugin">3 <property name="someProperty" value="100"/>4 </plugin>5</plugins>Q: 为什么MyBatis的插件只能拦截这四种接口?
A: MyBatis只允许拦截这四个接口是基于以下考虑:
- 架构设计:这四个接口是MyBatis执行SQL的核心组件,覆盖了SQL执行的完整生命周期
- 安全性:限制拦截点减少对核心功能的干扰
- 可控性:避免过度的扩展导致框架行为不可预测
- 性能考虑:减少代理层级,降低性能开销
这四个接口足以满足大多数扩展需求:
- Executor:整体SQL执行过程(缓存、事务等)
- StatementHandler:SQL语句处理(预处理、执行)
- ParameterHandler:参数设置(类型转换、参数映射)
- ResultSetHandler:结果集处理(结果映射、对象创建)
如需更深层次的定制,可以通过继承或修改MyBatis源码实现。
Q: 常用的MyBatis扩展有哪些?
A: 常用的MyBatis扩展:
-
MyBatis-Plus:
- 增强的CRUD操作
- 条件构造器
- 分页插件
- 代码生成器
- 乐观锁插件
- 字段自动填充
-
PageHelper:
- 强大的分页插件
- 多数据库支持
- 简单的API
- 灵活的分页参数
-
通用Mapper:
- 通用的CRUD方法
- Example查询条件
- 批量操作支持
- 灵活的条件查询
-
MyBatis-Spring:
- 集成Spring事务管理
- 自动Mapper扫描
- SqlSession管理
- 依赖注入支持
-
MyBatis-Dynamic-SQL:
- 类型安全的SQL构建
- 动态条件支持
- 避免硬编码SQL
Q: 分页插件PageHelper的工作原理是什么?
A: PageHelper分页插件的工作原理:
- 拦截执行器:拦截Executor的query方法
- 获取分页参数:从ThreadLocal中获取Page对象
- 解析原SQL:使用JSqlParser解析SQL
- 构造分页SQL:根据数据库类型添加分页语法
- 执行分页查询:先执行count查询获取总数,再执行分页查询
- 设置分页结果:将分页信息设置到Page对象
工作流程示意:
1调用PageHelper.startPage() → 创建Page对象并存入ThreadLocal →2执行查询方法 → 插件拦截 → 构建分页SQL → 3执行count查询 → 执行分页查询 → 返回结果 →4清除ThreadLocalQ: 如何实现自定义的结果集处理?
A: 自定义结果集处理有以下方式:
- 自定义TypeHandler:
1public class JsonTypeHandler<T> extends BaseTypeHandler<T> {2 private Class<T> clazz;3 private ObjectMapper objectMapper = new ObjectMapper();4 5 public JsonTypeHandler(Class<T> clazz) {6 this.clazz = clazz;7 }8 9 @Override10 public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {11 // 实现参数设置逻辑12 }13 14 @Override15 public T getNullableResult(ResultSet rs, String columnName) throws SQLException {16 // 实现结果获取逻辑17 }18 19 // 其他方法实现...20}2122// 注册TypeHandler23<typeHandlers>24 <typeHandler handler="com.example.JsonTypeHandler" javaType="com.example.Config" jdbcType="VARCHAR"/>25</typeHandlers>- 自定义ResultHandler:
1public class CustomResultHandler implements ResultHandler<User> {2 private List<User> users = new ArrayList<>();3 4 @Override5 public void handleResult(ResultContext<? extends User> resultContext) {6 User user = resultContext.getResultObject();7 // 自定义处理逻辑8 users.add(user);9 10 // 可以提前停止结果集处理11 if (users.size() >= 100) {12 resultContext.stop();13 }14 }15 16 public List<User> getUsers() {17 return users;18 }19}2021// 使用自定义ResultHandler22CustomResultHandler handler = new CustomResultHandler();23sqlSession.select("getUserList", params, handler);24List<User> users = handler.getUsers();- 拦截ResultSetHandler:
1@Intercepts({2 @Signature(3 type = ResultSetHandler.class,4 method = "handleResultSets",5 args = {Statement.class}6 )7})8public class ResultSetInterceptor implements Interceptor {9 @Override10 public Object intercept(Invocation invocation) throws Throwable {11 // 拦截结果集处理过程12 Statement stmt = (Statement) invocation.getArgs()[0];13 14 // 执行原方法15 Object result = invocation.proceed();16 17 // 处理结果18 if (result instanceof List) {19 List<?> list = (List<?>) result;20 // 对结果进行后处理21 }22 23 return result;24 }25 26 // 其他方法实现...27}- 理解原理:深入理解MyBatis的核心原理和执行流程
- 对比框架:了解MyBatis与其他ORM框架的区别和适用场景
- 性能优化:掌握常见的性能问题和优化方法
- 实践案例:准备实际项目中的使用案例和问题解决方案
- 源码分析:对关键组件的源码有基本了解
- 扩展机制:熟悉插件机制和常用扩展
参与讨论