MyBatis 作为 Java 生态中最流行的持久层框架之一,以其轻量级、高灵活性和易扩展性赢得了广大开发者的青睐。作为一名有着多年 MyBatis 使用经验的开发者,我将在本文中系统性地分享从基础 CRUD 操作到高级特性的实战经验,帮助大家真正掌握这个强大的 ORM 框架。
在实际项目中,MyBatis 能够显著简化数据库操作,提高开发效率。不同于 Hibernate 的全自动 ORM 映射,MyBatis 采用半自动的方式,让开发者既能享受到 ORM 的便利,又能保持对 SQL 的完全控制。这种设计理念使得 MyBatis 特别适合需要精细控制 SQL 性能的项目场景。
在开始使用 MyBatis 前,我们需要先搭建好基础环境。对于 Maven 项目,首先需要在 pom.xml 中添加必要的依赖:
xml复制<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
这里我推荐使用 MyBatis 3.5.13 版本,这是一个经过充分验证的稳定版本。MySQL 驱动则根据你的数据库版本选择,如果是 MySQL 8.x,建议使用 8.0.x 系列的驱动。
实际项目中,我通常会加上 Lombok 依赖来简化实体类代码,但这并非必须。如果你选择使用 Lombok,记得在 IDE 中安装对应的插件。
MyBatis 的核心配置文件(通常命名为 mybatis-config.xml)是整个框架的神经中枢。让我们来看一个完整的配置示例:
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="cacheEnabled" value="true"/>
</settings>
<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"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
在这个配置中,有几个关键点值得注意:
mapUnderscoreToCamelCase 设置为 true 可以自动将数据库的下划线命名转换为 Java 的驼峰命名,这在实际开发中非常实用。
数据源配置中,我推荐使用 POOLED 类型,这是 MyBatis 内置的连接池实现。对于生产环境,你可能需要考虑使用更专业的连接池如 HikariCP。
在 MySQL 8.0 的 JDBC URL 中,记得添加 useSSL=false 参数,除非你确实配置了 SSL。
MyBatis 的核心架构围绕几个关键组件构建,理解这些组件的职责和交互方式对于高效使用 MyBatis 至关重要。
SqlSessionFactory 是 MyBatis 的核心工厂类,负责创建 SqlSession 实例。它的创建成本较高,通常一个应用只需要一个实例,因此应该使用单例模式管理。
SqlSession 代表一次数据库会话,它是线程不安全的,因此最佳实践是在方法内部创建,使用完毕后立即关闭。我强烈推荐使用 try-with-resources 语法来确保 SqlSession 的正确关闭:
java复制try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 执行数据库操作
}
Mapper 接口 是 MyBatis 的精华所在。MyBatis 会通过动态代理技术为这些接口生成实现类,将 Java 方法调用转换为 SQL 语句执行。这种设计使得我们的代码能够保持接口的简洁性,同时获得强大的数据库操作能力。
Executor 是 SqlSession 内部的 SQL 执行引擎,它负责 SQL 语句的预处理、参数绑定、结果映射等核心操作。MyBatis 提供了三种执行器:
XML 映射是 MyBatis 最传统也是最强大的方式,特别适合复杂的 SQL 场景。让我们通过一个完整的用户管理示例来演示。
首先定义实体类:
java复制@Data
public class User {
private Long id;
private String username;
private String password;
private String email;
private Date createTime;
private Date updateTime;
}
然后创建 Mapper 接口:
java复制public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int deleteById(Long id);
}
对应的 Mapper XML 文件:
xml复制<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectById" resultMap="userResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, password, email)
VALUES(#{username}, #{password}, #{email})
</insert>
</mapper>
在这个 XML 配置中,有几个关键点需要注意:
namespace 必须与 Mapper 接口的全限定名完全一致,这是 MyBatis 将接口方法与 XML 语句关联的关键。
useGeneratedKeys="true" 和 keyProperty="id" 配合使用可以获取数据库自动生成的主键值,并回填到实体对象中。
参数使用 #{} 语法引用,这是 MyBatis 的参数占位符,可以有效防止 SQL 注入。
对于简单的 CRUD 操作,使用注解可以让代码更加简洁。MyBatis 提供了一系列注解来替代 XML 配置:
java复制@Mapper
public interface UserAnnotationMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO user(username, password, email) " +
"VALUES(#{username}, #{password}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE user SET username=#{username}, email=#{email} WHERE id=#{id}")
int update(User user);
@Delete("DELETE FROM user WHERE id=#{id}")
int deleteById(Long id);
}
注解方式的优点是简洁直观,适合简单的 SQL 语句。但对于复杂的动态 SQL,XML 方式仍然是更好的选择。
在实际项目中,我通常会将这两种方式结合使用:简单的 CRUD 使用注解,复杂的查询和动态 SQL 使用 XML 配置。
经过多个项目的实践,我总结出以下 CRUD 操作的最佳实践:
参数传递:当方法有多个参数时,使用 @Param 注解明确指定参数名,避免潜在的参数绑定问题。
结果映射:对于复杂的查询结果,使用 <resultMap> 进行显式映射,而不是依赖自动映射。这样可以提高代码的可读性和可维护性。
批量操作:对于批量插入或更新,考虑使用 Batch 执行器,可以显著提高性能。
事务管理:明确事务边界,对于需要事务的操作,确保在同一个 SqlSession 中完成。
异常处理:合理处理 MyBatis 抛出的异常,特别是 PersistenceException,提供有意义的错误信息。
MyBatis 提供了一套强大的动态 SQL 标签,让我们能够根据不同条件灵活构建 SQL 语句。这些标签包括:
<if>:条件判断<choose>、<when>、<otherwise>:多条件选择<where>:智能处理 WHERE 子句<set>:智能处理 UPDATE 语句<foreach>:遍历集合让我们通过几个实际场景来演示动态 SQL 的强大功能。
场景一:多条件查询
xml复制<select id="selectByCondition" resultMap="userResultMap">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
<where> 标签会智能处理 WHERE 子句,它会:
场景二:选择性更新
xml复制<update id="updateSelective">
UPDATE user
<set>
<if test="username != null">username=#{username},</if>
<if test="email != null">email=#{email},</if>
update_time=NOW()
</set>
WHERE id=#{id}
</update>
<set> 标签会:
场景三:批量插入
xml复制<insert id="batchInsert">
INSERT INTO user(username, password, email)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email})
</foreach>
</insert>
<foreach> 标签可以遍历集合,生成批量操作的 SQL。这在处理大量数据时非常有用。
测试表达式:<if> 标签的 test 属性支持强大的 OGNL 表达式,可以进行复杂的条件判断。
参数处理:在动态 SQL 中,可以直接使用传入的参数对象的属性,无需额外声明。
SQL 片段复用:使用 <sql> 标签定义可重用的 SQL 片段,然后在其他语句中通过 <include> 引用,提高代码复用性。
避免 SQL 注入:始终使用 #{} 语法进行参数绑定,而不是字符串拼接 (${}),除非你确实需要直接插入 SQL 片段。
MyBatis 提供了一级缓存和二级缓存两种缓存机制,合理使用可以显著提高应用性能。
一级缓存:
二级缓存:
xml复制<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>
在实际项目中,我通常会在配置类或配置文件中全局开启二级缓存,然后在具体的 Mapper 上按需配置缓存策略。
延迟加载(懒加载)是 MyBatis 针对关联查询的优化策略,核心思想是"按需加载"。
配置方式:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
关联查询示例:
xml复制<resultMap id="userWithOrdersResultMap" type="User">
<!-- 用户基础字段映射 -->
<collection property="orders" column="id"
select="com.example.mapper.OrderMapper.selectByUserId"
fetchType="lazy"/>
</resultMap>
使用场景:
注意事项:
MyBatis 插件通过拦截器机制实现功能扩展,可以拦截以下四大对象的方法:
开发一个简单的 SQL 执行时间统计插件:
java复制@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlCostTimeInterceptor implements Interceptor {
@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];
System.out.println("执行 [" + ms.getId() + "] 耗时: " + cost + "ms");
}
}
@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>
插件开发注意事项:
<foreach> 实现批量插入/更新<resultMap> 的嵌套结果或嵌套查询Mapper 接口无法注入:
参数绑定失败:
缓存不一致:
延迟加载异常:
java复制@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return sessionFactory.getObject();
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer scanner = new MapperScannerConfigurer();
scanner.setBasePackage("com.example.mapper");
return scanner;
}
}
Spring Boot 提供了开箱即用的 MyBatis 集成:
xml复制<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
properties复制mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.entity
事务不生效:
循环依赖:
多数据源配置:
在一个电商项目中,我们需要实现一个复杂的商品搜索功能,涉及多表关联、多种筛选条件和排序方式。最初实现的查询性能很差,执行时间超过 2 秒。通过以下优化手段,最终将查询时间降低到 200ms 以内:
SQL 重构:将复杂的 JOIN 查询拆分为多个简单查询,利用 MyBatis 的关联映射功能在内存中组装结果
索引优化:为常用查询条件添加复合索引
缓存策略:对不常变动的数据(如商品分类)启用二级缓存
分页优化:使用基于游标的分页代替传统的 LIMIT offset, size
在数据迁移任务中,需要处理数百万条数据的插入。最初使用单条插入,耗时超过 2 小时。通过以下改进,将时间缩短到 15 分钟:
java复制SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : userList) {
mapper.insert(user);
}
session.commit();
} finally {
session.close();
}
调整批量大小:每 1000 条提交一次,平衡内存使用和性能
关闭自动提交:避免每条插入都自动提交事务
在多租户系统中,需要根据当前租户动态切换数据源。我们通过以下方式实现:
关键代码示例:
java复制public class TenantDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
MyBatis Generator 可以自动生成实体类、Mapper 接口和 XML 文件:
xml复制<generatorConfiguration>
<context id="mysql" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/test"
userId="root" password="password"/>
<javaModelGenerator targetPackage="com.example.entity"
targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="mapper"
targetProject="src/main/resources"/>
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.example.mapper"
targetProject="src/main/java"/>
<table tableName="user" domainObjectName="User"/>
</context>
</generatorConfiguration>
MyBatis-Plus 是 MyBatis 的增强工具,提供了许多开箱即用的功能:
使用示例:
java复制public interface UserMapper extends BaseMapper<User> {
// 自动拥有基本 CRUD 方法
}
// 条件查询示例
List<User> users = userMapper.selectList(
new QueryWrapper<User>()
.like("username", "张")
.gt("age", 18)
.orderByDesc("create_time")
);
经过多个项目的实践,我认为 MyBatis 最大的优势在于它完美平衡了灵活性和易用性。与全自动 ORM 框架相比,MyBatis 让开发者能够完全控制 SQL,这对于性能敏感的应用至关重要;而与直接使用 JDBC 相比,MyBatis 又大幅减少了样板代码,提高了开发效率。
在实际使用中,我有以下几点深刻体会:
XML 与注解的平衡:不要极端地只使用一种方式,应该根据场景灵活选择。我通常将简单的 CRUD 放在注解中,复杂的查询和动态 SQL 放在 XML 里。
缓存使用的谨慎:缓存是把双刃剑,用得好可以极大提升性能,用得不好会导致数据不一致问题。一定要充分理解一二级缓存的工作原理和失效机制。
插件开发的克制:虽然 MyBatis 插件很强大,但过度使用会影响框架的稳定性和性能。只在确实需要增强或修改框架行为时才使用插件。
SQL 的可维护性:随着项目发展,SQL 会变得越来越复杂。保持良好的组织和注释习惯,必要时将复杂 SQL 拆分成多个简单 SQL 在内存中组合。
与 Spring 的深度集成:现代 Java 项目大多基于 Spring,熟练掌握 MyBatis-Spring 集成的最佳实践非常重要,特别是事务管理和多数据源场景。
最后,MyBatis 的学习曲线相对平缓,但要真正掌握它的精髓需要在实际项目中不断实践和总结。希望本文的内容能够帮助读者少走弯路,快速成为 MyBatis 使用高手。