在 Java 企业级应用开发中,持久层框架承担着数据访问的核心职责,而 MyBatis 凭借其 "半自动化" 的特性 ------ 既保留 SQL 的灵活性,又简化 JDBC 的繁琐操作 ------ 成为众多项目的首选。与 Hibernate 等全自动 ORM 框架相比,MyBatis 允许开发者直接编写 SQL 语句,更适合复杂查询场景和对性能有极致要求的系统。本文将从 MyBatis 的核心原理出发,系统讲解映射配置、动态 SQL、缓存机制、插件开发等高级特性,并结合电商、金融等实战场景,提供一套完整的 MyBatis 性能优化方案,帮助开发者构建高效、可维护的数据访问层。
一、MyBatis 核心原理:从配置到 SQL 执行的完整链路
要熟练使用 MyBatis,首先需要理解其底层工作流程。MyBatis 的核心在于 "配置驱动 SQL 执行",整个过程可分为初始化、SQL 解析、参数处理、执行与结果映射四个阶段,每个阶段都有明确的组件分工。
1. 初始化阶段:构建核心配置体系
MyBatis 的初始化始于SqlSessionFactory的创建,该过程通过加载核心配置文件(mybatis-config.xml)和映射文件(Mapper.xml),构建起全局唯一的配置体系。核心配置文件主要包含数据源、事务管理器、类型别名、插件等全局配置,而映射文件则定义 SQL 语句与 Java 方法的映射关系。
初始化关键步骤如下:
- 通过Resources.getResourceAsStream()加载 mybatis-config.xml 文件;
- 创建SqlSessionFactoryBuilder,调用build()方法解析配置文件,生成Configuration对象(MyBatis 的核心配置载体,存储所有配置信息);
- 由Configuration对象创建SqlSessionFactory,该对象是线程安全的,通常在应用启动时初始化一次。
示例代码如下:
// MyBatis初始化示例
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
在 Spring 整合 MyBatis 的场景中,初始化过程由 Spring 容器托管,通过SqlSessionFactoryBean自动完成配置加载和SqlSessionFactory创建,开发者无需手动处理,只需在配置类中注入数据源即可:
@Configuration
@MapperScan("com.example.mapper") // 扫描Mapper接口
public class MyBatisConfig {
@Autowired
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 设置映射文件路径
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/**/*.xml")
);
return sessionFactory.getObject();
}
}
2. SQL 执行阶段:动态解析与参数绑定
当调用SqlSession的方法执行 SQL 时,MyBatis 会经历 "SQL 解析 - 参数绑定 - 执行 - 结果映射" 的完整流程,核心依赖Executor、StatementHandler、ParameterHandler、ResultSetHandler四大组件:
- Executor:执行器,负责管理缓存和调度 SQL 执行,分为BaseExecutor(基础执行器,含一级缓存)和CachingExecutor(缓存执行器,含二级缓存);
- StatementHandler:处理 JDBC 的Statement对象,负责 SQL 语句的预处理和执行;
- ParameterHandler:处理 SQL 参数,将 Java 对象转换为 JDBC 参数;
- ResultSetHandler:处理查询结果,将 JDBC 的ResultSet转换为 Java 对象。
以查询用户信息为例,SQL 执行的关键流程如下:
- 通过SqlSession.getMapper(UserMapper.class)获取 Mapper 接口的代理对象(MyBatis 通过 JDK 动态代理生成);
- 调用userMapper.selectById(1)时,代理对象会根据方法全限定名(如com.example.mapper.UserMapper.selectById)在Configuration中找到对应的MappedStatement(存储 SQL 语句、参数类型、结果类型等信息);
- Executor根据MappedStatement创建StatementHandler,并由ParameterHandler将参数 "1" 绑定到 SQL 的?占位符;
- StatementHandler执行 SQL,获取ResultSet,再由ResultSetHandler将结果映射为User对象并返回。
这一流程的核心优势在于 "动态性"------MyBatis 会根据参数类型和 SQL 语句的结构,自动处理参数绑定和结果映射,避免了 JDBC 中手动设置参数、遍历结果集的繁琐代码。
二、MyBatis 核心特性:从基础映射到动态 SQL
MyBatis 的灵活性体现在其丰富的配置特性上,从基础的结果映射到复杂的动态 SQL,再到高级的关联查询,每一项特性都针对实际开发中的痛点设计,帮助开发者高效编写数据访问代码。
1. 结果映射:解决 "字段名与属性名不匹配" 问题
在数据库表设计中,字段名通常采用 "下划线命名法"(如user_name),而 Java 实体类属性采用 "驼峰命名法"(如userName),这种不匹配会导致 MyBatis 无法自动映射结果。MyBatis 提供了三种解决方案:
(1)通过resultMap手动配置映射
这是最灵活的方式,适用于字段与属性映射关系复杂的场景,例如:
UserResultMap" type="com.example.entity.User">
_id" property="userId"/> _name" property="userName"/> -->
="user_age" property="userAge"/>
" property="createTime" jdbcType="TIMESTAMP"/>
<select id="selectById" resultMap="UserResultMap">
SELECT user_id, user_name, user_age, create_time
FROM t_user
WHERE user_id = #{userId}
(2)开启驼峰命名自动映射
在 mybatis-config.xml 中配置mapUnderscoreToCamelCase为true,MyBatis 会自动将 "下划线命名" 的字段转换为 "驼峰命名" 的属性,无需手动配置resultMap:
batis-config.xml -->
<settings>
name="mapUnderscoreToCamelCase" value="true"/>
>
语句可直接使用resultType -->
selectById" resultType="com.example.entity.User">
SELECT user_id, user_name, user_age, create_time
FROM t_user
WHERE user_id = #{userId}
</select>
(3)使用别名映射
在 SQL 语句中通过AS为字段设置别名,使其与 Java 属性名一致,适用于临时映射场景:
selectById" resultType="com.example.entity.User">
SELECT
user_id AS userId,
user_name AS userName,
user_age AS userAge,
create_time AS createTime
FROM t_user
WHERE user_id = #{userId}
</select>
2. 动态 SQL:应对复杂查询条件的 "利器"
在实际开发中,查询条件往往是动态变化的(如电商平台的多条件筛选),若通过 Java 代码拼接 SQL,不仅繁琐且易引发 SQL 注入风险。MyBatis 的动态 SQL 通过<if>、<choose>、、,允许在 XML 中编写逻辑判断,动态生成 SQL 语句,既安全又高效。
(1)<if>与<where>:条件判断与 SQL 拼接
例如,实现 "根据用户名和年龄查询用户" 的动态查询,当用户名或年龄为空时,自动忽略对应的条件:
id="selectByCondition" resultType="com.example.entity.User">
SELECT * FROM t_user
<where> AND/OR,避免SQL语法错误 -->
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="userAge != null">
AND user_age = #{userAge}
</select>
移除条件中多余的AND或OR,避免生成SELECT * FROM t_user WHERE AND user_age = 20` 这类语法错误。
(2)<foreach>:批量操作与集合参数处理
批量查询(如根据多个用户 ID 查询)或批量插入场景中,可将Java集合转换为SQL中的IN` 条件或值列表:
-->
selectByIds" resultType="com.example.entity.User">
SELECT * FROM t_user
WHERE user_id IN
="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
Insert">
INSERT INTO t_user (user_name, user_age, create_time)
VALUES
item="user" separator=",">
(#{user.userName}, #{user.userAge}, #{user.createTime})
其中,`collection`指定集合参数的名称(若为List则默认`list`,若为数组则默认`array`,若为Map则指定Key),`item`为集合元素的别名,`open`和`close`指定SQL片段的开头和结尾,`separator`指定元素间的分隔符。
#### (3)`分支判断
类似Java中的`if-else`,适用于"多个条件中仅满足一个"的场景,例如"优先根据用户ID查询,若ID为空则根据用户名查询":
```````xml````
id="selectByChoose" resultType="com.example.entity.User">
SELECT * FROM t_user
<where>
>
="userId != null">
AND user_id = #{userId}
>
="userName != null and userName != ''">
AND user_name = #{userName}
>
AND user_age > 18
</otherwise>
</choose>
>
3. 关联查询:处理一对一与一对多关系
在数据库设计中,表与表之间常存在关联关系(如 "用户 - 订单" 的一对多关系、"订单 - 支付记录" 的一对一关系)。MyBatis 通过>(一对一)和<collection>(一对多)标签,实现关联对象的嵌套映射,避免手动编写多表查询后的结果组装代码。
(1)一对一关联:association
以 "订单 - 支付记录" 为例,一个订单对应一条支付记录,映射如下:
实体类:包含Payment属性 -->
public class Order {
private Long orderId;
private String orderNo;
private Payment payment; // 一对一关联的支付记录
// getter/setter
}
OrderWithPaymentResultMap" type="com.example.entity.Order">
="order_id" property="orderId"/>
<result column="order_no" property="orderNo"/>
-->
property="payment" javaType="com.example.entity.Payment">
="pay_id" property="payId"/>
<result column="pay_amount" property="payAmount"/>
_time" property="payTime"/>
>
OrderWithPayment" resultMap="OrderWithPaymentResultMap">
SELECT
o.order_id, o.order_no,
p.pay_id, p.pay_amount, p.pay_time
FROM t_order o
LEFT JOIN t_payment p ON o.order_id = p.order_id
WHERE o.order_id = #{orderId}
>
(2)一对多关联:collection
以 "用户 - 订单" 为例,一个用户对应多个订单,映射如下:
:包含List属性 -->
public class User {
private Long userId;
private String userName;
private List<Order> orders; // 一对多关联的订单列表
// getter/setter
}
Mapper.xml -->
Map id="UserWithOrdersResultMap" type="com.example.entity.User">
" property="userId"/>
_name" property="userName"/>
多关联映射 -->
ofType="com.example.entity.Order">
<id column="order_id" property="orderId"/>
" property="orderNo"/>
order_time" property="orderTime"/>
>
UserWithOrders" resultMap="UserWithOrdersResultMap">
SELECT
u.user_id, u.user_name,
o.order_id, o.order_no, o.order_time
FROM t_user u
LEFT JOIN t_order o ON u.user_id = o.user_id
WHERE u.user_id = #{userId}
</select>
需注意,association的javaType指定关联对象的类型,而collection的ofType指定集合中元素的类型,避免混淆。
三、MyBatis 缓存机制:提升查询性能的关键手段
缓存是优化数据库查询性能的核心手段,MyBatis 提供了一级缓存(本地缓存)和二级缓存(全局缓存),通过减少数据库访问次数,大幅提升高频查询的响应速度。
1. 一级缓存:SqlSession 级别的本地缓存
一级缓存是 MyBatis 的默认缓存,作用域为SqlSession(即一次数据库会话)。当在同一个SqlSession中执行相同的 SQL 查询时,MyBatis 会将第一次查询的结果存入一级缓存,后续查询直接从缓存中获取,无需再次访问数据库。
一级缓存的核心特性:
- 默认开启,无需额外配置;
- 缓存的 key 由MappedStatement的 ID(即 SQL 的唯一标识)、SQL 语句、参数、分页信息等共同决定;
- 当执行insert、update、delete操作或调用SqlSession.clearCache()时,一级缓存会被清空,避免缓存数据与数据库数据不一致。
示例代码验证一级缓存:
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询:访问数据库,结果存入一级缓存
User user1 = mapper.selectById(1);
// 第二次查询:相同SqlSession,从一级缓存获取,不访问数据库
User user2 = mapper.selectById(1);
System.out.println(user1 == user2); // true(同一对象)
}
2. 二级缓存:SqlSessionFactory 级别的全局缓存
二级缓存的作用域为SqlSessionFactory,即多个SqlSession共享缓存数据,适用于多线程共享高频查询结果的场景(如电商平台的商品列表查询)。二级缓存需要手动开启,且实体类需实现Serializable接口(缓存数据需序列化存储)。
(1)二级缓存的开启步骤
- 在 mybatis-config.xml 中开启二级缓存全局配置(默认已开启,可显式配置):
>
="cacheEnabled" value="true"/>
- 在需要使用二级缓存的 Mapper.xml 中添加 ` 标签:
<!-- UserMapper.xml -->
<cache
eviction="LRU" 缓存淘汰策略:LRU(最近最少使用) -->
flushInterval="60000" 缓存刷新间隔:60秒 -->
size="1024" -->
readOnly="false"/> false表示可写入 -->
- 确保实体类实现Serializable接口:
public class User implements Serializable {
// 字段与 getter/setter
}
(2)二级缓存的执行流程
- 第一个SqlSession执行查询后,结果会先存入一级缓存;
- 当SqlSession提交(commit())或关闭(close())时,一级缓存的数据会被同步到二级缓存;
- 第二个SqlSession执行相同查询时,会先从二级缓存获取数据,若缓存命中则直接返回,否则访问数据库并更新缓存。
示例代码验证二级缓存:
// 第一个SqlSession
try (SqlSession session1 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1); // 访问数据库
session1.commit(); // 提交后,一级缓存同步到二级缓存
}
// 第二个SqlSession
try (SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1); // 从二级缓存获取,不访问数据库
}
(3)二级缓存的注意事项
- 二级缓存不适用于数据频繁修改的场景(如订单表),否则会因缓存频繁刷新导致性能下降;
- 若 Mapper.xml 中某条 SQL 不需要使用二级缓存,可在>标签中设置useCache="false":
="selectById" resultType="User" useCache="false">
SELECT * FROM t_user WHERE user_id = #{userId}
</select>
- 执行insert/update/delete操作时,MyBatis 会自动清空当前 Mapper 的二级缓存,可通过flushCache="false"禁用(不推荐,可能导致数据不一致)。
四、MyBatis 性能优化:从 SQL 到配置的全方位调优
在高并发场景下(如电商秒杀、金融交易),MyBatis 的性能直接影响系统整体吞吐量。性能优化需从 SQL 优化、参数配置、插件使用三个维度入手,结合实际业务场景制定方案。
1. SQL 优化:减少数据库访问开销
SQL 优化是 MyBatis 性能优化的基础,低效的 SQL 会导致数据库压力剧增,即使缓存配置完善也无法弥补。常见的 SQL 优化手段包括:
(1)避免全表扫描,使用索引
确保查询条件中的字段(如WHERE、ORDER BY、JOIN关联字段)已建立索引,例如 "根据用户 ID 查询订单" 时,t_order表的user_id字段应建立索引。同时,避免使用SELECT *,而是显式指定需要查询的字段,减少数据传输量:
优化前:全表扫描,查询所有字段 -->
ByUserId" resultType="Order">
SELECT * FROM t_order WHERE user_id = #{userId}
</select>
后:使用索引,只查询必要字段 -->
<select id="selectByUserId" resultType="Order">
SELECT order_id, order_no, order_time
FROM t_order
WHERE user_id = #{userId} 索引 -->
(2)优化关联查询,避免笛卡尔积
多表关联查询时,需确保JOIN条件明确,避免因关联条件缺失导致笛卡尔积(数据量呈指数级增长)。例如 "用户 - 订单 - 支付记录" 三表关联,需指定清晰的JOIN条件:
="selectUserOrderPayment" resultMap="UserOrderPaymentResultMap">
SELECT
u.user_id, u.user_name,
o.order_id, o.order_no,
p.pay_id, p.pay_amount
FROM t_user u
LEFT JOIN t_order o ON u.user_id = o.user_id 明确关联条件 -->
LEFT JOIN t_payment p ON o.order_id = p.order_id 条件 -->
WHERE u.user_id = #{userId}
#### (3)使用分页查询,避免大量数据加载
查询结果集较大时(如超过100条),需使用分页查询,避免一次性加载大量数据导致内存溢出。MyBatis的分页可通过`RowBounds`(内存分页,不推荐)或插件(如PageHelper,物理分页,推荐)实现:
```````java````
// 使用PageHelper实现物理分页(推荐)
PageHelper.startPage(1, 10); // 第1页,每页10条
List> userList = userMapper.selectAll(); // 自动拼接LIMIT语句
PageInfo new PageInfo);
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());
物理分页通过在 SQL 语句后拼接LIMIT(MySQL)或ROWNUM(Oracle),直接从数据库获取分页数据,避免内存分页的性能问题。
2. 配置优化:提升 MyBatis 运行效率
MyBatis 的核心配置参数对性能影响显著,合理配置可减少资源消耗,提升执行效率。
(1)数据源配置:使用连接池
MyBatis 默认使用简易的连接池,生产环境中需替换为性能更优的第三方连接池(如 HikariCP、Druid),减少数据库连接创建与销毁的开销。以 HikariCP 为例,配置如下:
-->
production">
Manager type="JDBC"/>
="POOLED">
CP的数据源类 -->
SourceClassName" value="com.zaxxer.hikari.HikariDataSource"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis_db"/>
value="root"/>
value="123456"/>
<property name="maximumPoolSize" value="20"/>
minimumIdle" value="5"/> 数 -->
name="idleTimeout" value="300000"/> 连接超时时间 -->
</dataSource>
</environment>
HikariCP 是目前性能最优的 Java 连接池,相比 C3P0、DBCP,其连接获取速度更快,内存消耗更低。
(2)懒加载配置:延迟加载关联对象
在关联查询中,若暂时不需要关联对象的数据,可开启懒加载(延迟加载),仅当访问关联对象时才执行关联查询,减少不必要的数据库访问。配置如下:
Enabled" value="true"/> -->
="aggressiveLazyLoading" value="false"/> 关闭积极懒加载,按需加载 -->
>
例如,查询用户时不立即加载订单列表,仅当调用user.getOrders()时才执行订单查询 SQL,适用于 "大部分场景只需用户基本信息" 的业务需求。
(3)参数映射优化:使用#而非$
MyBatis 的参数占位符有#和$两种:
- #:预编译占位符,会将参数转换为?,通过PreparedStatement设置参数,可防止 SQL 注入,性能更优;
- $:字符串替换占位符,直接将参数拼接到 SQL 中,存在 SQL 注入风险,仅适用于动态表名、动态字段名等特殊场景。
开发中应优先使用#,避免$的安全隐患:
:使用#,预编译 -->
selectById" resultType="User">
SELECT * FROM t_user WHERE user_id = #{userId}
</select>
推荐:使用$,存在SQL注入风险(仅特殊场景使用) -->
<select id="selectByTableName" resultType="User">
SELECT * FROM ${tableName} WHERE user_id = #{userId}
</select>
3. 插件开发:扩展 MyBatis 功能
MyBatis 的插件机制允许开发者在 SQL 执行的关键节点(如参数处理、结果映射、SQL 执行)插入自定义逻辑,实现日志记录、性能监控、数据加密等功能。MyBatis 插件基于 JDK 动态代理实现,可拦截的四大核心组件为:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
(1)插件开发步骤
以 "SQL 执行时间监控插件" 为例,开发流程如下:
- 实现Interceptor接口,重写intercept()(核心逻辑)、plugin()(生成代理对象)、setProperties()(配置参数)方法:
@Intercepts({
@Signature(
type = Executor.class, // 拦截的组件类型
method = "query", // 拦截的方法
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} // 方法参数
)
})
public class SqlExecutionTimeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 记录开始时间
long startTime = System.currentTimeMillis();
try {
// 2. 执行原方法(SQL查询)
return invocation.proceed();
} finally {
// 3. 计算执行时间并打印日志
long costTime = System.currentTimeMillis() - startTime;
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId(); // 获取SQL的唯一标识
System.out.printf("SQL [%s] 执行时间:%d ms%n", sqlId, costTime);
}
}
@Override
public Object plugin(Object target) {
// 生成代理对象,仅对Executor组件生效
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取插件配置参数(如日志级别)
}
}
- 在 mybatis-config.xml 中注册插件:
>
ceptor="com.example.plugin.SqlExecutionTimeInterceptor">
) -->
name="logLevel" value="INFO"/>
</plugin>
- 启动应用后,执行 SQL 时会自动打印执行时间,便于定位慢查询。
(2)插件使用注意事项
- 插件会生成代理对象,过度使用可能导致性能下降,建议仅在必要场景(如监控、加密)使用;
- 拦截方法时需明确参数类型,避免因参数不匹配导致拦截失败;
- 多个插件同时拦截同一组件时,需通过@Order注解指定执行顺序,避免逻辑冲突。
五、MyBatis 实战案例:电商订单系统的数据访问层设计
以电商订单系统为例,结合前文讲解的 MyBatis 特性,设计一套高效、可维护的数据访问层,涵盖订单查询、创建、批量操作等核心场景。
1. 需求分析与表设计
订单系统的核心表包括t_order(订单主表)、t_order_item(订单明细表)、t_payment(支付记录表),表结构简化如下:
- t_order:order_id(主键)、order_no(订单编号)、user_id(用户 ID)、order_amount(订单金额)、order_status(订单状态)、create_time(创建时间);
- t_order_item:item_id(主键)、order_id(订单 ID)、product_id(商品 ID)、product_name(商品名称)、quantity(购买数量)、price(商品单价);
- t_payment:pay_id(主键)、order_id(订单 ID)、pay_amount(支付金额)、pay_type(支付方式)、pay_time(支付时间)。
2. Mapper 接口与映射文件设计
(1)订单查询:关联查询订单、明细、支付记录
// OrderMapper.java
public interface OrderMapper {
// 查询订单及其明细、支付记录
OrderVO selectOrderDetail(@Param("orderId") Long orderId);
}
// OrderMapper.xml
OrderDetailResultMap" type="com.example.vo.OrderVO">
<id column="order_id" property="orderId"/>
" property="orderNo"/>
order_amount" property="orderAmount"/>
column="order_status" property="orderStatus"/>
-->
property="payment" javaType="com.example.entity.Payment">
="pay_id" property="payId"/>
<result column="pay_amount" property="payAmount"/>
_type" property="payType"/>
>
多关联订单明细 -->
Items" ofType="com.example.entity.OrderItem">
_id" property="itemId"/>
="product_id" property="productId"/>
<result column="product_name" property="productName"/>
" property="quantity"/>
" property="price"/>
</collection>
</resultMap>
id="selectOrderDetail" resultMap="OrderDetailResultMap">
SELECT
o.order_id, o.order_no, o.order_amount, o.order_status,
p.pay_id, p.pay_amount, p.pay_type,
i.item_id, i.product_id, i.product_name, i.quantity, i.price
FROM t_order o
LEFT JOIN t_payment p ON o.order_id = p.order_id
LEFT JOIN t_order_item i ON o.order_id = i.order_id
WHERE o.order_id = #{orderId}
(2)订单创建:插入订单主表与明细表(事务控制)
// OrderMapper.java
public interface OrderMapper {
// 插入订单主表
int insertOrder(Order order);
// 批量插入订单明细表
int batchInsertOrderItem(@Param("orderItems") ListItems);
}
// OrderMapper.xml
id="insertOrder" useGeneratedKeys="true" keyProperty="orderId">
INSERT INTO t_order (order_no, user_id, order_amount, order_status, create_time)
VALUES (#{orderNo}, #{userId}, #{orderAmount}, #{orderStatus}, #{createTime})
InsertOrderItem">
INSERT INTO t_order_item (order_id, product_id, product_name, quantity, price)
VALUES
" item="item" separator=",">
(#{item.orderId}, #{item.productId}, #{item.productName}, #{item.quantity}, #{item.price})
</foreach>
</insert>
// Service层(事务控制)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order, List<OrderItem> orderItems) {
// 插入订单主表,获取自增主键orderId
orderMapper.insertOrder(order);
// 为明细设置orderId
orderItems.forEach(item -> item.setOrderId(order.getOrderId()));
// 批量插入明细
orderMapper.batchInsertOrderItem(orderItems);
}
}
(3)订单筛选:动态 SQL 多条件查询
// OrderMapper.java
public interface OrderMapper {
// 多条件筛选订单(支持用户ID、订单状态、时间范围)
List<OrderVO> selectOrderByFilter(OrderFilter filter);
}
// OrderFilter.java(筛选条件实体)
public class OrderFilter {
private Long userId;
private Integer orderStatus;
private Date startTime; // 下单开始时间
private Date endTime; // 下单结束时间
// getter/setter
}
// OrderMapper.xml
id="selectOrderByFilter" resultType="com.example.vo.OrderVO">
SELECT order_id, order_no, order_amount, order_status, create_time
FROM t_order
<where>
test="userId != null">
AND user_id = #{userId}
</if>
test="orderStatus != null">
AND order_status = #{orderStatus}
!= null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
ORDER BY create_time DESC
</select>
3. 性能优化措施
- 缓存优化:对 "订单状态查询" 等高频且不常修改的场景,开启二级缓存,缓存刷新间隔设置为 5 分钟;
- SQL 优化:为t_order表的user_id、order_status、create_time字段建立联合索引,提升多条件查询效率;
- 连接池优化:使用 HikariCP,设置最大连接数为 30,最小空闲连接数为 10,适配订单系统的并发需求;
- 分页优化:订单列表查询使用 PageHelper 实现物理分页,避免一次性加载大量订单数据。
六、MyBatis 常见问题与解决方案
在实际开发中,MyBatis 常遇到 "参数绑定错误""结果映射失败""缓存数据不一致" 等问题,以下是高频问题的解决方案:
1. 参数绑定错误:There is no getter for property named 'xxx' in 'class java.lang.Integer'
原因:当方法参数为单个基本类型或包装类型时,MyBatis 默认将参数名称视为param1,若在 XML 中使用#{xxx}(如#{userId}),会因无法找到对应的 getter 方法报错。
解决方案:
- 使用@Param注解为参数指定名称:
// 错误写法
List<User> selectByUserId(Integer userId); // XML中使用#{userId}会报错
// 正确写法
List<User> selectByUserId(@Param("userId") Integer userId); // XML中可使用#{userId}
- 在 XML 中使用#{param1}替代#{xxx}: