1. 为什么我们需要MyBatis?
作为一个常年和数据库打交道的Java开发者,我至今还记得早期使用纯JDBC时那些令人抓狂的日子。每次都要手动管理数据库连接、处理异常、遍历ResultSet... 光是写一个简单的查询就得几十行模板代码。直到遇到MyBatis,这种痛苦才真正结束。
1.1 JDBC的痛点与ORM的救赎
让我们先看一段典型的JDBC查询代码:
java复制// JDBC查询示例
public User findById(int id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
// 1. 获取连接
conn = DriverManager.getConnection(url, username, password);
// 2. 创建Statement
ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setInt(1, id);
// 3. 执行查询
rs = ps.executeQuery();
// 4. 处理结果集
if (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
// 其他字段...
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5. 关闭资源
try {
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return user;
}
这段代码暴露了JDBC的几个核心问题:
- 样板代码过多:每个查询都要重复连接管理、异常处理等流程
- SQL硬编码:SQL语句直接写在Java代码中,修改需要重新编译
- 手动映射繁琐:需要手动从ResultSet中提取数据并设置到对象
- 资源管理复杂:必须确保Connection、Statement等资源正确关闭
1.2 MyBatis的优雅解决方案
现在看看用MyBatis实现同样功能的代码:
java复制// MyBatis Mapper接口
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(int id);
}
// 使用方式
User user = sqlSession.getMapper(UserMapper.class).findById(1);
对比之下,MyBatis的优势一目了然:
- 代码量减少90%:只需定义接口和方法,无需实现类
- SQL与Java代码分离:SQL可以写在XML或注解中
- 自动结果映射:自动将查询结果转换为Java对象
- 内置连接池:无需手动管理数据库连接
1.3 MyBatis的定位与适用场景
MyBatis是一个半自动ORM框架,介于全自动ORM(如Hibernate)和纯JDBC之间。这种设计带来了独特的优势:
| 特性 | JDBC | MyBatis | Hibernate |
|---|---|---|---|
| SQL控制度 | 完全控制 | 完全控制 | 有限控制 |
| 开发效率 | 最低 | 中等 | 最高 |
| 学习曲线 | 简单 | 中等 | 陡峭 |
| 性能调优 | 困难 | 容易 | 困难 |
根据我的经验,MyBatis特别适合:
- 需要精细控制SQL的项目
- 遗留数据库系统(表设计不符合规范)
- 对性能要求较高的场景
- 团队SQL能力较强的项目组
2. 环境搭建与核心配置
2.1 项目初始化与依赖配置
我推荐使用Maven来管理项目依赖。以下是完整的pom.xml配置:
xml复制<!-- MyBatis核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>
<!-- 日志框架(推荐使用SLF4J+Logback) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
注意:实际开发中建议使用dependencyManagement统一管理版本号,这里为演示简化了配置。
2.2 数据库准备
创建测试数据库和表结构:
sql复制CREATE DATABASE IF NOT EXISTS mybatis_demo
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE mybatis_demo;
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(50) NOT NULL COMMENT '密码',
age INT COMMENT '年龄',
email VARCHAR(100) COMMENT '邮箱',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
我特意做了几点优化:
- 使用utf8mb4字符集支持完整Unicode(包括emoji)
- 添加了适当的索引提高查询效率
- 增加了update_time字段记录最后修改时间
- 添加了表注释和字段注释
2.3 MyBatis核心配置文件详解
mybatis-config.xml是MyBatis的主配置文件,建议放在resources目录下:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局设置 -->
<settings>
<!-- 开启下划线转驼峰 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 打印查询语句 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<!-- 事务管理器 -->
<transactionManager type="JDBC"/>
<!-- 数据源 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!-- 连接池配置 -->
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="10"/>
</dataSource>
</environment>
</environments>
<!-- 映射器配置 -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
</configuration>
关键配置说明:
-
settings:全局行为设置
- mapUnderscoreToCamelCase:自动将数据库字段user_name映射到Java属性userName
- logImpl:配置日志实现,推荐使用SLF4J
-
typeAliases:为Java类型设置短别名
- 配置后,在Mapper XML中可以用"User"代替"com.example.entity.User"
-
environments:支持多环境配置
- 可以通过修改default值切换环境
- 生产环境建议使用JNDI数据源
-
mappers:指定Mapper接口或XML文件位置
- 推荐使用包扫描方式(需接口与XML同名同包)
3. 核心开发流程实战
3.1 实体类设计
实体类是与数据库表映射的Java对象,设计时有几个要点:
java复制package com.example.entity;
import java.util.Date;
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String email;
private Date createTime;
private Date updateTime;
// 必须有无参构造
public User() {}
// getter/setter省略...
// 建议重写toString()
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
", email='" + email + '\'' +
", createTime=" + createTime +
", updateTime=" + updateTime +
'}';
}
}
设计规范:
- 属性名建议与数据库字段名对应(开启驼峰映射后会自动转换)
- 必须提供无参构造方法(MyBatis反射创建对象需要)
- 建议重写toString()方便调试
- 对于日期字段,推荐使用java.util.Date或java.time.*
3.2 Mapper接口设计
Mapper接口是MyBatis的核心抽象,定义了数据库操作方法:
java复制package com.example.mapper;
import com.example.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
// 根据ID查询
User findById(@Param("id") Integer id);
// 查询所有用户
List<User> findAll();
// 根据用户名模糊查询
List<User> findByUsernameLike(@Param("username") String username);
// 插入用户
int insert(User user);
// 批量插入
int batchInsert(@Param("users") List<User> users);
// 更新用户
int update(User user);
// 删除用户
int deleteById(@Param("id") Integer id);
}
接口设计建议:
- 方法名应见名知意,遵循查询(find/select)、插入(insert/add)、更新(update)、删除(delete/remove)的命名约定
- 参数建议使用@Param注解明确指定名称
- 返回类型要合理:查询返回对象或集合,修改操作返回受影响行数
3.3 SQL映射文件编写
UserMapper.xml是对应接口的SQL实现:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 结果映射配置 -->
<resultMap id="userResultMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- 根据ID查询 -->
<select id="findById" resultMap="userResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 查询所有用户 -->
<select id="findAll" resultMap="userResultMap">
SELECT * FROM user
</select>
<!-- 根据用户名模糊查询 -->
<select id="findByUsernameLike" resultMap="userResultMap">
SELECT * FROM user WHERE username LIKE CONCAT('%', #{username}, '%')
</select>
<!-- 插入用户 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, age, email)
VALUES (#{username}, #{password}, #{age}, #{email})
</insert>
<!-- 批量插入 -->
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, age, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.age}, #{user.email})
</foreach>
</insert>
<!-- 更新用户 -->
<update id="update">
UPDATE user
SET username = #{username},
password = #{password},
age = #{age},
email = #{email}
WHERE id = #{id}
</update>
<!-- 删除用户 -->
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
关键点说明:
- namespace必须与Mapper接口全限定名一致
- resultMap用于定义结果集映射关系
- useGeneratedKeys="true"表示使用数据库自增主键
- keyProperty指定主键属性名
- foreach标签用于实现批量操作
3.4 工具类封装
封装一个MyBatisUtils工具类可以简化SqlSession的获取:
java复制package com.example.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
throw new RuntimeException("初始化MyBatis失败", e);
}
}
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
public static SqlSession getSqlSession(boolean autoCommit) {
return sqlSessionFactory.openSession(autoCommit);
}
public static <T> T getMapper(Class<T> type) {
return sqlSessionFactory.openSession(true).getMapper(type);
}
}
这个工具类提供了三种获取SqlSession的方式:
- 默认获取需要手动提交事务的SqlSession
- 可指定是否自动提交事务
- 直接获取Mapper接口实例(自动提交事务)
3.5 单元测试
编写JUnit测试验证各个功能:
java复制package com.example.mapper;
import com.example.entity.User;
import com.example.util.MyBatisUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class UserMapperTest {
private SqlSession sqlSession;
private UserMapper userMapper;
@Before
public void setUp() {
sqlSession = MyBatisUtils.getSqlSession();
userMapper = sqlSession.getMapper(UserMapper.class);
}
@After
public void tearDown() {
if (sqlSession != null) {
sqlSession.close();
}
}
@Test
public void testFindById() {
User user = userMapper.findById(1);
System.out.println(user);
}
@Test
public void testFindAll() {
List<User> users = userMapper.findAll();
users.forEach(System.out::println);
}
@Test
public void testInsert() {
User user = new User();
user.setUsername("test");
user.setPassword("123456");
user.setAge(20);
user.setEmail("test@example.com");
int rows = userMapper.insert(user);
sqlSession.commit();
System.out.println("插入行数:" + rows + ", 生成ID:" + user.getId());
}
@Test
public void testBatchInsert() {
User user1 = new User(null, "batch1", "111", 21, "b1@test.com", null, null);
User user2 = new User(null, "batch2", "222", 22, "b2@test.com", null, null);
int rows = userMapper.batchInsert(Arrays.asList(user1, user2));
sqlSession.commit();
System.out.println("批量插入行数:" + rows);
System.out.println("生成ID1:" + user1.getId() + ", ID2:" + user2.getId());
}
@Test
public void testUpdate() {
User user = userMapper.findById(1);
user.setEmail("new_email@test.com");
int rows = userMapper.update(user);
sqlSession.commit();
System.out.println("更新行数:" + rows);
}
@Test
public void testDelete() {
int rows = userMapper.deleteById(10);
sqlSession.commit();
System.out.println("删除行数:" + rows);
}
}
测试注意事项:
- 增删改操作需要手动提交事务(或使用自动提交的SqlSession)
- 测试完成后要关闭SqlSession释放资源
- 可以使用@Before和@After注解简化测试代码
4. 高级特性与最佳实践
4.1 动态SQL
MyBatis提供了强大的动态SQL功能,可以根据不同条件生成不同的SQL语句。常用的动态SQL标签包括:
xml复制<!-- 条件查询示例 -->
<select id="findByCondition" resultMap="userResultMap">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
ORDER BY id DESC
</select>
<!-- 选择性更新示例 -->
<update id="updateSelective">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
动态SQL最佳实践:
<where>标签会自动处理AND/OR前缀问题<set>标签会自动去除多余的逗号- 对于复杂的条件判断,可以使用
<choose>、<when>、<otherwise>组合 - 大量相似条件考虑使用
<sql>定义可重用的SQL片段
4.2 结果映射高级用法
MyBatis的结果映射非常灵活,可以处理各种复杂场景:
xml复制<!-- 复杂结果映射示例 -->
<resultMap id="detailedUserResultMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="user_name"/>
<!-- 其他基础字段... -->
<!-- 一对一关联 -->
<association property="department" javaType="Department">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
</association>
<!-- 一对多关联 -->
<collection property="roles" ofType="Role">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
</collection>
</resultMap>
关联查询建议:
- 对于性能要求高的场景,建议使用多次查询代替复杂的联表查询
- 可以使用
@One和@Many注解实现延迟加载 - 复杂的嵌套结果可以考虑使用
ResultHandler自定义处理
4.3 缓存机制
MyBatis提供两级缓存提高查询性能:
-
一级缓存(本地缓存)
- 默认开启,作用域为SqlSession
- 同一个SqlSession中相同的查询会直接返回缓存结果
- 执行任何INSERT/UPDATE/DELETE操作都会清空缓存
-
二级缓存(全局缓存)
- 需要手动开启,作用域为Mapper namespace
- 配置方式:
xml复制<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> - 注意事项:
- 实体类需要实现Serializable接口
- 多个Mapper共享缓存时要注意命名空间配置
缓存使用建议:
- 查询多修改少的表适合开启二级缓存
- 高并发系统要谨慎使用二级缓存
- 对于实时性要求高的数据,可以设置较短的flushInterval
4.4 插件开发
MyBatis允许通过插件拦截核心组件的方法调用,常见的拦截点包括:
- Executor (update, query, flushStatements等)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
开发一个简单的SQL执行时间统计插件:
java复制@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class SqlCostTimeInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
logger.info("SQL执行耗时[{}ms] - {}", cost, ms.getId());
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以读取配置参数
}
}
然后在配置文件中注册插件:
xml复制<plugins>
<plugin interceptor="com.example.plugin.SqlCostTimeInterceptor"/>
</plugins>
插件开发注意事项:
- 不要过度使用插件,会影响性能
- 注意拦截方法的签名必须完全匹配
- 线程安全考虑,避免在插件中使用共享状态
5. 生产环境最佳实践
5.1 分页查询实现
MyBatis有多种分页实现方式:
-
内存分页(不推荐):
java复制
List<User> users = userMapper.findAll(); List<User> page = users.stream() .skip(offset).limit(pageSize) .collect(Collectors.toList()); -
SQL分页(推荐):
xml复制<select id="findByPage" resultMap="userResultMap"> SELECT * FROM user ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select> -
PageHelper插件(最方便):
java复制// 使用PageHelper PageHelper.startPage(pageNum, pageSize); List<User> users = userMapper.findAll(); PageInfo<User> pageInfo = new PageInfo<>(users);
分页建议:
- 大数据量分页避免使用
LIMIT offset, size,推荐使用"上一页最大ID"方式 - 使用PageHelper时要注意调用位置(必须在查询方法前调用)
5.2 批量操作优化
批量操作能显著提高性能,MyBatis支持多种批量处理方式:
-
foreach批量插入:
xml复制<insert id="batchInsert"> INSERT INTO user (username, password) VALUES <foreach collection="list" item="item" separator=","> (#{item.username}, #{item.password}) </foreach> </insert> -
BatchExecutor:
java复制try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper = session.getMapper(UserMapper.class); for (int i = 0; i < 1000; i++) { mapper.insert(new User("user" + i, "pass" + i)); } session.commit(); } -
rewriteBatchedStatements:
在JDBC URL中添加rewriteBatchedStatements=true可以显著提升MySQL批量操作性能
批量操作建议:
- 大批量插入考虑使用LOAD DATA INFILE
- 合理设置批量大小(通常500-1000条一批)
- 注意事务大小,避免单个事务太大
5.3 多数据源配置
大型项目经常需要访问多个数据库,配置多数据源的方法:
-
定义多个数据源配置:
java复制@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } } -
配置多个SqlSessionFactory:
java复制@Bean public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } -
使用注解切换数据源:
java复制@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String value() default "primary"; } // 使用AOP实现数据源切换 @Around("@annotation(dataSource)") public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable { String key = dataSource.value(); try { DynamicDataSourceContextHolder.setDataSourceKey(key); return point.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSourceKey(); } }
多数据源使用建议:
- 明确划分数据源的读写职责
- 注意事务管理器的配置
- 考虑使用ShardingSphere等中间件处理复杂场景
5.4 监控与调优
生产环境MyBatis监控建议:
-
SQL监控:
- 使用Druid等连接池提供的SQL监控功能
- 配置慢SQL阈值并监控
-
日志配置:
xml复制<logger name="org.mybatis" level="DEBUG"/> <logger name="java.sql" level="DEBUG"/> -
性能调优:
- 合理设置连接池参数
- 优化复杂查询,添加适当索引
- 使用二级缓存要谨慎
-
常见问题排查:
- 参数为null:检查参数传递是否正确
- 结果映射失败:检查resultMap配置
- SQL语法错误:先在数据库客户端测试SQL
6. 常见问题解决方案
6.1 参数绑定问题
问题现象:参数没有正确绑定到SQL中,导致查询结果不正确或报错
解决方案:
-
使用@Param注解明确指定参数名:
java复制User findByUsernameAndAge( @Param("username") String username, @Param("age") Integer age); -
复杂参数使用Map封装:
java复制List<User> findByCondition(@Param("params") Map<String, Object> params); -
检查参数类型是否匹配:
- 日期类型要特别注意格式问题
- 枚举类型需要特殊处理
6.2 结果映射问题
问题现象:查询返回的实体类部分属性为null
解决方案:
-
确认数据库字段名和Java属性名是否匹配
-
检查是否开启了驼峰映射:
xml复制<setting name="mapUnderscoreToCamelCase" value="true"/> -
使用显式的resultMap:
xml复制<resultMap id="userResultMap" type="User"> <result column="create_time" property="createTime"/> </resultMap> -
SQL中使用别名:
sql复制SELECT create_time AS createTime FROM user
6.3 事务管理问题
问题现象:增删改操作没有生效,数据没有变化
解决方案:
-
确保调用了SqlSession的commit()方法:
java复制try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); mapper.insert(user); session.commit(); // 必须提交事务 } -
或者使用自动提交的SqlSession:
java复制try (SqlSession session = sqlSessionFactory.openSession(true)) { UserMapper mapper = session.getMapper(UserMapper.class); mapper.insert(user); // 自动提交 } -
Spring集成环境下,使用@Transactional注解管理事务
6.4 性能问题
问题现象:系统响应慢,数据库负载高
优化建议:
-
检查是否使用了N+1查询问题:
- 使用join查询代替多次查询
- 或者使用批量查询
-
合理使用缓存:
- 考虑开启二级缓存
- 对于热点数据使用Redis等外部缓存
-
优化复杂查询:
- 添加适当的数据库索引
- 重构复杂SQL
-
批量操作代替循环单条操作
6.5 其他常见异常
-
BindingException:
- 检查Mapper接口和XML文件是否对应
- 检查方法名是否匹配
-
TooManyResultsException:
- 查询返回了多行但方法声明返回单个对象
- 修改方法返回类型为List或添加LIMIT 1
-
PersistenceException:
- 检查数据库连接配置
- 检查SQL语法是否正确
7. MyBatis与Spring集成
虽然MyBatis可以独立使用,但在Spring环境中集成会更加方便。以下是集成步骤:
7.1 添加Spring依赖
xml复制<!-- Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<!-- MyBatis-Spring整合包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Spring JDBC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.18</version>
</dependency>
7.2 配置Spring Bean
java复制@Configuration
@ComponentScan("com.example")
public class AppConfig {
// 数据源
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(
"jdbc:mysql://localhost:3306/mybatis_demo",
"root", "123456");
}
// SqlSessionFactory
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
// 其他配置...
return factoryBean.getObject();
}
// Mapper扫描
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.example.mapper");
return configurer;
}
// 事务管理器
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
7.3 使用@Transactional管理事务
java复制@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void createUser(User user) {
userMapper.insert(user);
// 其他操作...
}
}
集成优势:
- 自动注入Mapper接口
- 声明式事务管理
- 与Spring其他功能无缝集成
8. MyBatis 3新特性
MyBatis 3.x版本引入了一些有用的新特性:
8.1 注解SQL
对于简单的SQL,可以直接使用注解而不用XML:
java复制public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(int id);
@Insert("INSERT INTO user(username, password) VALUES(#{username}, #{password})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE user SET username=#{username} WHERE id=#{id}")
int update(User user);
@Delete("DELETE FROM user WHERE id=#{id}")
int delete(int id);
}
8.2 动态SQL构建器
Java代码中构建动态SQL:
java复制@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);
class UserSqlBuilder {
public static String buildGetUsersByName(final String name) {
return new SQL(){{
SELECT("*");
FROM("user");
if (name != null) {
WHERE("username LIKE #{name} || '%'");
}
ORDER_BY("id");
}}.toString();
}
}
8.3 结果处理器
自定义结果集处理:
java复制@Select("SELECT * FROM user")
@Results(id = "userResult", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "username", column = "username")
})
@TypeDiscriminator(column = "type", javaType = String.class, cases = {
@Case(value = "ADMIN", type = Admin.class),
@Case(value = "USER", type = User.class)
})
List<User> findAllUsers();
9. 实际项目经验分享
9.1 项目目录结构规范
推荐的项目结构:
code复制src/main/java
├── com.example
│ ├── entity # 实体类
│ ├── mapper # Mapper接口
│ ├── service # 业务逻辑
│ └── controller # 控制器
src/main/resources
├── mapper # XML映射文件
├── application.yml # 配置文件
└── mybatis-config.xml
9.2 开发流程建议
- 先设计数据库表结构
- 创建对应的实体类
- 定义Mapper接口方法
- 编写SQL映射文件
- 编写单元测试验证
- 实现业务逻辑
9.3 性能优化案例
案例:用户列表查询缓慢
优化过程:
- 发现查询使用了
SELECT *,但实际只需要部分字段 - 添加适当的索引