文章目录
1. MyBatis简介
1.1 什么是MyBatis
MyBatis是一个优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis几乎消除了JDBC代码和参数的手动设置以及结果集的检索。MyBatis使用简单的XML或注解进行配置,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射到数据库中的记录。
与其他ORM框架不同,MyBatis并没有将Java对象与数据库表关联起来,而是将方法与SQL语句关联。MyBatis让开发人员可以使用更自然的面向对象的方式来操作数据库。
1.2 MyBatis解决的问题
- 简化JDBC操作:MyBatis大大简化了JDBC操作,减少了手动编写JDBC代码的工作量。
- SQL与代码分离:MyBatis将SQL语句与Java代码分离,便于维护和管理。
- 参数映射:自动将Java对象映射到SQL语句的参数中。
- 结果映射:自动将SQL查询结果映射为Java对象。
- 支持动态SQL:可以根据不同的条件动态生成SQL语句。
- 缓存机制:提供一级缓存和二级缓存,提高查询效率。
1.3 与JDBC、Hibernate的对比
特性 | JDBC | MyBatis | Hibernate |
---|---|---|---|
开发效率 | 低 | 中 | 高 |
灵活性 | 高 | 高 | 中 |
学习曲线 | 陡峭 | 平缓 | 较陡峭 |
SQL控制 | 完全手动 | 自定义 | 自动生成 |
性能 | 依赖优化 | 高 | 中 |
映射关系 | 无 | SQL映射 | 对象-关系映射 |
1.4 基本工作流程概览
MyBatis的基本工作流程如下:
- 应用程序调用MyBatis的API访问数据库
- MyBatis根据配置文件创建SqlSessionFactory
- 通过SqlSessionFactory获取SqlSession实例
- SqlSession完成数据库操作
- 业务逻辑处理完成后关闭SqlSession
// 基本工作流程示例代码
// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 打开SqlSession
SqlSession session = sqlSessionFactory.openSession();
try {
// 4. 执行SQL语句
User user = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
// 5. 处理结果
System.out.println(user.getName());
} finally {
// 6. 关闭SqlSession
session.close();
}
2. MyBatis核心组件
MyBatis框架的核心组件构成了其工作原理的基础。每个组件都有其特定的功能和作用,它们协同工作,完成从Java对象到数据库操作的转换。
2.1 SqlSessionFactoryBuilder
SqlSessionFactoryBuilder是MyBatis的入口,用于构建SqlSessionFactory实例。它可以从XML配置文件或Configuration类构建SqlSessionFactory。
// 从XML文件创建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 从Java配置类创建SqlSessionFactory
Configuration configuration = new Configuration();
// ... 配置属性和映射
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSessionFactoryBuilder的作用就是读取配置信息并创建SqlSessionFactory对象。一旦创建了SqlSessionFactory,就不再需要SqlSessionFactoryBuilder了,它可以被回收或销毁。
2.2 SqlSessionFactory
SqlSessionFactory是MyBatis的核心接口,它用于创建SqlSession实例。SqlSessionFactory的生命周期应该与应用程序的生命周期相同,通常情况下,我们只需要一个SqlSessionFactory实例。
// 创建默认的SqlSession
SqlSession session = sqlSessionFactory.openSession();
// 创建自动提交的SqlSession
SqlSession autoCommitSession = sqlSessionFactory.openSession(true);
// 创建指定事务隔离级别的SqlSession
SqlSession isolatedSession = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
// 创建指定执行器类型的SqlSession
SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
SqlSessionFactory是线程安全的,可以被多个线程共享。它通常在应用程序启动时创建,并在应用程序结束时销毁。
2.3 SqlSession
SqlSession是MyBatis的主要接口,通过它可以执行SQL命令、获取映射器(Mapper)、管理事务。SqlSession是线程不安全的,每个线程应该有自己的SqlSession实例。
SqlSession session = sqlSessionFactory.openSession();
try {
// 执行SQL语句
List<User> users = session.selectList("org.mybatis.example.UserMapper.getAllUsers");
// 插入数据
User newUser = new User("Tom", "tom@example.com");
session.insert("org.mybatis.example.UserMapper.insertUser", newUser);
// 更新数据
User userToUpdate = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
userToUpdate.setName("Updated Name");
session.update("org.mybatis.example.UserMapper.updateUser", userToUpdate);
// 删除数据
session.delete("org.mybatis.example.UserMapper.deleteUser", 2);
// 提交事务
session.commit();
} catch (Exception e) {
// 回滚事务
session.rollback();
throw e;
} finally {
// 关闭SqlSession
session.close();
}
SqlSession的生命周期应该是请求作用域的,即一个请求一个SqlSession,请求结束后关闭SqlSession。
2.4 Mapper接口
Mapper接口是MyBatis中用于定义数据库操作的接口。MyBatis会为Mapper接口创建动态代理对象,通过代理对象执行SQL语句。
// 定义Mapper接口
public interface UserMapper {
User getUserById(int id);
List<User> getAllUsers();
void insertUser(User user);
void updateUser(User user);
void deleteUser(int id);
}
// 使用Mapper接口
SqlSession session = sqlSessionFactory.openSession();
try {
UserMapper userMapper = session.getMapper(UserMapper.class);
// 查询
User user = userMapper.getUserById(1);
List<User> allUsers = userMapper.getAllUsers();
// 插入
User newUser = new User("Tom", "tom@example.com");
userMapper.insertUser(newUser);
// 更新
user.setName("Updated Name");
userMapper.updateUser(user);
// 删除
userMapper.deleteUser(2);
session.commit();
} finally {
session.close();
}
Mapper接口的好处是可以使用Java接口和方法,而不必直接使用字符串调用SQL语句,这样可以提供编译时类型检查,减少运行时错误。
2.5 Executor
Executor是MyBatis的核心接口之一,负责执行SQL语句。MyBatis有三种内置的Executor类型:
- SIMPLE:默认的Executor,每执行一次更新操作(update、insert、delete)就提交一次事务。
- REUSE:重用预处理语句(PreparedStatement)。
- BATCH:批量执行所有更新语句。
// 在配置文件中设置默认的Executor类型
<settings>
<setting name="defaultExecutorType" value="REUSE" />
</settings>
// 在代码中指定Executor类型
SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
虽然Executor是MyBatis内部的接口,但了解它的作用有助于理解MyBatis的工作原理和优化查询性能。
2.6 StatementHandler
StatementHandler负责处理JDBC Statement的创建、设置参数、执行SQL语句以及获取结果。
// MyBatis内部的StatementHandler接口
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout);
void parameterize(Statement statement);
void batch(Statement statement);
int update(Statement statement);
<E> List<E> query(Statement statement, ResultHandler resultHandler);
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
StatementHandler将Java对象转换为JDBC可以执行的Statement对象,它处理了从Java参数到SQL参数的转换,以及从SQL结果到Java对象的转换。
2.7 ParameterHandler
ParameterHandler负责将用户传入的参数转换为JDBC Statement中需要的参数。
// MyBatis内部的ParameterHandler接口
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps);
}
ParameterHandler处理SQL语句中的占位符(?)和实际参数值之间的映射关系,它将Java对象的属性值设置到JDBC PreparedStatement中。
2.8 ResultSetHandler
ResultSetHandler负责将JDBC返回的ResultSet结果集转换为Java对象。
// MyBatis内部的ResultSetHandler接口
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt);
<E> Cursor<E> handleCursorResultSets(Statement stmt);
void handleOutputParameters(CallableStatement cs);
}
ResultSetHandler将数据库查询结果映射为Java对象,它处理了从数据库字段到Java对象属性的转换。
3. MyBatis工作流程详解
MyBatis的工作流程可以分为初始化阶段和执行阶段两部分。
3.1 初始化阶段
初始化阶段主要完成以下工作:
- 解析配置文件:MyBatis首先解析mybatis-config.xml配置文件,读取全局配置和映射文件位置。
- 解析映射文件:解析所有Mapper.xml映射文件,建立SQL语句和对应方法的映射关系。
- 创建会话工厂:创建SqlSessionFactory实例,准备生成SqlSession。
// 初始化阶段示例代码
// 1. 读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 2. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 3. 创建SqlSessionFactory对象
SqlSessionFactory factory = builder.build(inputStream);
// 至此,初始化阶段完成
在初始化阶段,MyBatis会解析所有XML配置文件,构建内部的Configuration对象,该对象包含了MyBatis运行所需的所有配置信息。
3.1.1 配置文件解析过程
MyBatis会按照以下顺序解析配置文件中的元素:
- properties:读取属性配置文件。
- settings:全局参数设置。
- typeAliases:类型别名。
- typeHandlers:类型处理器。
- objectFactory:对象工厂。
- plugins:插件。
- environments:环境配置(数据源、事务管理器)。
- databaseIdProvider:数据库厂商标识。
- mappers:映射器。
<!-- mybatis-config.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>
<!-- 属性配置 -->
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
<!-- 全局参数设置 -->
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<typeAlias alias="User" type="org.mybatis.example.User"/>
<package name="org.mybatis.example"/>
</typeAliases>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- 映射器 -->
<mappers>
<mapper resource="org/mybatis/example/UserMapper.xml"/>
<package name="org.mybatis.example"/>
</mappers>
</configuration>
3.1.2 映射文件解析过程
映射文件(Mapper.xml)定义了SQL语句和参数映射、结果映射等。MyBatis会解析这些文件,构建内存中的映射关系。
<!-- UserMapper.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="org.mybatis.example.UserMapper">
<select id="getUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="getAllUsers" resultType="User">
SELECT * FROM users
</select>
<insert id="insertUser" parameterType="User">
INSERT INTO users (name, email) VALUES (#{name}, #{email})
</insert>
<update id="updateUser" parameterType="User">
UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
解析过程中,MyBatis会将每个SQL语句解析为一个MappedStatement对象,该对象包含了SQL语句的id、参数映射、结果映射、SQL语句类型等信息。
3.2 执行阶段
执行阶段主要完成以下工作:
- 创建会话:从SqlSessionFactory获取SqlSession。
- 绑定映射器:获取Mapper接口的代理对象。
- 执行SQL:调用Mapper方法,执行对应的SQL语句。
- 处理结果:将查询结果映射为Java对象。
- 释放资源:关闭SqlSession。
// 执行阶段示例代码
// 1. 从SqlSessionFactory获取SqlSession
SqlSession session = factory.openSession();
try {
// 2. 获取Mapper接口代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
// 3. 执行SQL语句
User user = userMapper.getUserById(1);
// 4. 处理结果
System.out.println("User found: " + user.getName());
// 执行更新操作
user.setName("New Name");
userMapper.updateUser(user);
// 提交事务
session.commit();
} catch (Exception e) {
// 回滚事务
session.rollback();
} finally {
// 5. 关闭SqlSession
session.close();
}
3.2.1 Mapper代理对象的创建
当我们调用session.getMapper(UserMapper.class)
时,MyBatis会为UserMapper接口创建一个动态代理对象。代理对象会拦截接口方法的调用,转而执行对应的SQL语句。
// MyBatis内部为Mapper创建代理对象的示意代码
public class MapperProxy<T> implements InvocationHandler {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
// 构造方法等省略...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果是Object类方法,直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 从缓存中获取MapperMethod
MapperMethod mapperMethod = methodCache.computeIfAbsent(method,
k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
// 执行SQL语句
return mapperMethod.execute(sqlSession, args);
}
}
3.2.2 SQL语句的执行流程
当调用Mapper接口的方法时,MyBatis会执行以下步骤:
- 获取MappedStatement:根据方法的完全限定名找到对应的MappedStatement。
- 创建Executor:根据配置创建Executor实例。
- 创建StatementHandler:根据SQL类型创建StatementHandler实例。
- 创建ParameterHandler:负责处理参数映射。
- 创建ResultSetHandler:负责处理结果集映射。
- 执行SQL:调用JDBC API执行SQL语句。
- 处理结果:将ResultSet映射为Java对象。
// 以查询为例,MyBatis内部执行SQL的简化流程
// 1. 从Configuration获取MappedStatement
MappedStatement ms = configuration.getMappedStatement("org.mybatis.example.UserMapper.getUserById");
// 2. 创建执行器
Executor executor = configuration.newExecutor(transaction, executorType);
// 3. 创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
// 4. 准备Statement
Statement stmt = handler.prepare(connection, transactionTimeout);
// 5. 设置参数
handler.parameterize(stmt);
// 6. 执行查询
List<User> users = handler.query(stmt, resultHandler);
// 7. 关闭Statement
stmt.close();
3.2.3 一次完整的查询示例分析
以下是一个完整的查询操作示例,从调用Mapper方法到返回结果的全过程:
// 应用代码
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
// 内部执行流程:
// 1. 获取MapperProxy代理对象
// 2. 调用MapperProxy.invoke方法
// 3. 获取MapperMethod对象
// 4. 调用MapperMethod.execute方法
// 5. 根据方法类型(SELECT)调用SqlSession.selectOne方法
// 6. SqlSession委托给Executor执行查询
// 7. Executor创建StatementHandler
// 8. StatementHandler创建Statement并设置参数
// 9. StatementHandler执行查询并返回结果集
// 10. ResultSetHandler将结果集映射为User对象
// 11. 返回User对象给应用代码
这个流程展示了MyBatis如何将一个简单的接口方法调用转换为复杂的数据库操作,同时隐藏了底层的JDBC细节。
4. 配置文件详解
MyBatis的配置文件是其工作的基础,主要包括主配置文件(mybatis-config.xml)和映射文件(Mapper.xml)。
4.1 主配置文件
mybatis-config.xml是MyBatis的全局配置文件,它定义了MyBatis的行为方式。
4.1.1 properties元素
properties元素用于定义属性,这些属性可以在整个配置文件中被引用。
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
<!-- 引用属性 -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
properties元素可以从外部文件加载属性,也可以直接在配置文件中定义属性。
4.1.2 settings元素
settings元素用于更改MyBatis的运行时行为。
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
这些设置会改变MyBatis的行为,例如启用缓存、懒加载、自动映射等。
4.1.3 typeAliases元素
typeAliases元素用于为Java类型定义别名,简化XML配置。
<typeAliases>
<typeAlias alias="User" type="org.mybatis.example.User"/>
<!-- 批量扫描别名 -->
<package name="org.mybatis.example"/>
</typeAliases>
使用typeAlias可以为单个类定义别名,而使用package可以为整个包中的类定义别名。
4.1.4 typeHandlers元素
typeHandlers元素用于定义类型处理器,它们负责Java类型和JDBC类型之间的转换。
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
<!-- 批量扫描类型处理器 -->
<package name="org.mybatis.example"/>
</typeHandlers>
MyBatis已经为常见的Java类型提供了默认的类型处理器,但我们也可以自定义类型处理器来处理特殊类型。
4.1.5 objectFactory元素
objectFactory元素用于自定义对象工厂,它负责创建结果对象实例。
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
大多数情况下,我们不需要自定义对象工厂,使用默认的对象工厂就足够了。
4.1.6 plugins元素
plugins元素用于定义MyBatis插件,插件可以拦截MyBatis的某些方法调用,改变其行为。
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
MyBatis允许我们拦截以下方法的调用:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
4.1.7 environments元素
environments元素用于配置不同的环境,每个环境都需要一个事务管理器和一个数据源。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<environment id="production">
<transactionManager type="MANAGED"/>
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/MyBatisDemoDS"/>
</dataSource>
</environment>
</environments>
environments元素可以配置多个环境,通过default属性指定默认环境。
4.1.8 databaseIdProvider元素
databaseIdProvider元素用于支持不同的数据库,使MyBatis可以根据不同的数据库执行不同的SQL语句。
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
<property name="MySQL" value="mysql" />
</databaseIdProvider>
使用databaseIdProvider后,我们可以在映射文件中为不同的数据库编写不同的SQL语句。
4.1.9 mappers元素
mappers元素用于指定映射文件的位置,MyBatis会加载这些文件中的SQL映射。
<mappers>
<!-- 使用相对路径 -->
<mapper resource="org/mybatis/example/UserMapper.xml"/>
<!-- 使用URL -->
<mapper url="file:///var/mappers/UserMapper.xml"/>
<!-- 使用映射器接口的完全限定类名 -->
<mapper class="org.mybatis.example.UserMapper"/>
<!-- 批量扫描映射器 -->
<package name="org.mybatis.example"/>
</mappers>
mappers元素可以通过resource、url、class或package属性指定映射文件的位置。
4.2 映射文件
映射文件(Mapper.xml)用于定义SQL语句、参数映射和结果映射等。
4.2.1 mapper元素
mapper元素是映射文件的根元素,namespace属性指定了该映射文件对应的Mapper接口。
<?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="org.mybatis.example.UserMapper">
<!-- SQL映射语句 -->
</mapper>
namespace属性必须指定为Mapper接口的完全限定名,这样MyBatis才能将SQL映射语句与Mapper接口的方法关联起来。
4.2.2 select元素
select元素用于定义查询语句,它有很多属性用于配置查询行为。
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="getUsersByName" parameterType="string" resultType="User">
SELECT * FROM users WHERE name LIKE #{name}
</select>
<select id="getAllUsers" resultType="User">
SELECT * FROM users
</select>
select元素的常用属性包括:
- id:SQL语句的唯一标识,通常与Mapper接口的方法名相同。
- parameterType:参数的类型。
- resultType:结果的类型。
- resultMap:结果映射的ID。
- flushCache:是否刷新缓存。
- useCache:是否使用缓存。
- timeout:超时时间。
- fetchSize:获取记录的数量。
- statementType:语句类型(STATEMENT、PREPARED、CALLABLE)。
- resultSetType:结果集类型(FORWARD_ONLY、SCROLL_INSENSITIVE、SCROLL_SENSITIVE)。
4.2.3 insert、update、delete元素
insert、update、delete元素用于定义插入、更新和删除语句。
<insert id="insertUser" parameterType="User">
INSERT INTO users (name, email)
VALUES (#{name}, #{email})
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>
<update id="updateUser" parameterType="User">
UPDATE users SET
name = #{name},
email = #{email}
WHERE id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>
这些元素的常用属性与select元素类似,此外,insert元素还有一些特殊属性:
- useGeneratedKeys:是否使用JDBC的getGeneratedKeys方法获取主键。
- keyProperty:指定主键属性。
- keyColumn:指定主键列名。
insert元素中的selectKey子元素用于获取生成的主键值。
4.2.4 sql元素
sql元素用于定义可重用的SQL片段,可以被其他语句引用。
<sql id="userColumns">id, name, email</sql>
<select id="getUserById" parameterType="int" resultType="User">
SELECT <include refid="userColumns"/>
FROM users WHERE id = #{id}
</select>
<select id="getAllUsers" resultType="User">
SELECT <include refid="userColumns"/>
FROM users
</select>
使用sql元素可以减少重复代码,提高可维护性。
4.2.5 resultMap元素
resultMap元素用于定义结果映射,它可以将查询结果映射为复杂的对象图。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="userResultMap">
SELECT
user_id,
user_name,
user_email
FROM users WHERE user_id = #{id}
</select>
resultMap元素的常用子元素包括:
- id:映射主键列。
- result:映射普通列。
- association:映射一对一关联。
- collection:映射一对多关联。
- discriminator:根据条件映射不同的结果。
4.2.6 cache元素
cache元素用于配置该命名空间的缓存。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
cache元素的属性包括:
- eviction:缓存回收策略(LRU、FIFO、SOFT、WEAK)。
- flushInterval:刷新间隔,单位毫秒。
- size:缓存引用数量。
- readOnly:是否只读。
- blocking:是否阻塞。
- type:自定义缓存实现类。
5. 参数处理机制
MyBatis的参数处理机制负责将Java对象的属性值设置到SQL语句的参数占位符中。
5.1 #{}与${}的区别
MyBatis支持两种参数占位符:#{}和${}。
- #{}:预编译参数占位符,防止SQL注入。
- ${}:直接字符串替换,有SQL注入风险。
<!-- 使用#{} -->
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- 预编译SQL:SELECT * FROM users WHERE id = ? -->
<!-- 使用${} -->
<select id="getUsersByTableName" parameterType="string" resultType="User">
SELECT * FROM ${tableName}
</select>
<!-- 直接替换:SELECT * FROM users -->
一般情况下,应该优先使用#{},只有在需要动态改变表名、列名等情况下才使用${}。
5.2 简单参数
当Mapper方法接收单个参数时,可以直接使用参数名作为占位符名称。
// Mapper接口
public interface UserMapper {
User getUserById(int id);
}
// XML映射
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
对于单个参数,MyBatis不关心参数名,可以使用任意名称,如#{id}、#{value}或#{anyName}。
5.3 多个参数
当Mapper方法接收多个参数时,可以使用@Param注解为参数命名。
// Mapper接口
public interface UserMapper {
List<User> getUsersByNameAndEmail(@Param("name") String name, @Param("email") String email);
}
// XML映射
<select id="getUsersByNameAndEmail" resultType="User">
SELECT * FROM users WHERE name = #{name} AND email = #{email}
</select>
如果不使用@Param注解,MyBatis会将参数命名为arg0、arg1…或param1、param2…。
// 不使用@Param注解
public interface UserMapper {
List<User> getUsersByNameAndEmail(String name, String email);
}
// XML映射(使用arg名称)
<select id="getUsersByNameAndEmail" resultType="User">
SELECT * FROM users WHERE name = #{arg0} AND email = #{arg1}
</select>
// 或者使用param名称
<select id="getUsersByNameAndEmail" resultType="User">
SELECT * FROM users WHERE name = #{param1} AND email = #{param2}
</select>
为了提高可读性,建议使用@Param注解为参数命名。
5.4 JavaBean参数
当Mapper方法接收JavaBean对象作为参数时,可以使用属性名作为占位符名称。
// User类
public class User {
private int id;
private String name;
private String email;
// getter和setter方法
}
// Mapper接口
public interface UserMapper {
void insertUser(User user);
}
// XML映射
<insert id="insertUser" parameterType="User">
INSERT INTO users (name, email)
VALUES (#{name}, #{email})
</insert>
MyBatis会使用JavaBean的getter方法获取属性值,设置到SQL参数中。
5.5 Map参数
当Mapper方法接收Map对象作为参数时,可以使用Map的键作为占位符名称。
// Mapper接口
public interface UserMapper {
List<User> getUsersByCondition(Map<String, Object> condition);
}
// XML映射
<select id="getUsersByCondition" parameterType="map" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</select>
// 调用方式
Map<String, Object> condition = new HashMap<>();
condition.put("name", "Tom");
condition.put("email", "tom@example.com");
List<User> users = userMapper.getUsersByCondition(condition);
使用Map参数可以灵活地传递不同的参数,但缺点是类型不安全,可读性不高。
5.6 数组和集合参数
当Mapper方法接收数组或集合作为参数时,可以使用特定的名称访问元素。
// Mapper接口
public interface UserMapper {
List<User> getUsersByIds(List<Integer> ids);
}
// XML映射
<select id="getUsersByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
// 对于数组参数
<select id="getUsersByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
对于集合参数,MyBatis使用特定的名称:
- List类型:collection=“list”
- Array类型:collection=“array”
- 如果使用@Param注解命名,则使用注解的名称:collection=“ids”
5.7 特殊参数处理
有时需要处理一些特殊类型的参数,如日期、枚举等。
// 处理日期参数
<insert id="insertUserWithBirthday">
INSERT INTO users (name, birthday)
VALUES (#{name}, #{birthday})
</insert>
// 处理枚举参数
<insert id="insertUserWithStatus">
INSERT INTO users (name, status)
VALUES (#{name}, #{status})
</insert>
MyBatis内置了很多TypeHandler来处理常见类型的转换,如日期、枚举等。我们也可以自定义TypeHandler来处理特殊类型。
// 自定义枚举TypeHandler
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}
@Override
public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return UserStatus.fromCode(code);
}
@Override
public UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return UserStatus.fromCode(code);
}
@Override
public UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return UserStatus.fromCode(code);
}
}
// 注册TypeHandler
<typeHandlers>
<typeHandler handler="org.mybatis.example.UserStatusTypeHandler" javaType="org.mybatis.example.UserStatus"/>
</typeHandlers>
6. 结果映射机制
MyBatis的结果映射机制负责将数据库查询结果转换为Java对象。
6.1 自动映射
MyBatis可以自动将查询结果映射为JavaBean对象,只要列名与属性名匹配(忽略大小写)。
// User类
public class User {
private int id;
private String name;
private String email;
// getter和setter方法
}
// XML映射
<select id="getUserById" parameterType="int" resultType="User">
SELECT id, name, email FROM users WHERE id = #{id}
</select>
在这个例子中,MyBatis会自动将id列映射到id属性,name列映射到name属性,email列映射到email属性。
6.2 列名和属性名不匹配
当列名与属性名不匹配时,可以使用别名或resultMap解决。
<!-- 使用别名 -->
<select id="getUserById" parameterType="int" resultType="User">
SELECT
id AS id,
username AS name,
user_email AS email
FROM users WHERE id = #{id}
</select>
<!-- 使用resultMap -->
<resultMap id="userResultMap" type="User">
<id property="id" column="id" />
<result property="name" column="username"/>
<result property="email" column="user_email"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="userResultMap">
SELECT id, username, user_email
FROM users WHERE id = #{id}
</select>
resultMap元素的常用子元素包括:
- id:映射主键列。
- result:映射普通列。
- association:映射一对一关联。
- collection:映射一对多关联。
6.3 一对一关联
当需要映射一对一关联关系时,可以使用association元素。
// User类
public class User {
private int id;
private String name;
private String email;
private Address address;
// getter和setter方法
}
// Address类
public class Address {
private int id;
private String street;
private String city;
private String country;
// getter和setter方法
}
// XML映射
<resultMap id="userWithAddressMap" type="User">
<id property="id" column="user_id" />
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
<association property="address" javaType="Address">
<id property="id" column="addr_id"/>
<result property="street" column="street"/>
<result property="city" column="city"/>
<result property="country" column="country"/>
</association>
</resultMap>
<select id="getUserWithAddress" parameterType="int" resultMap="userWithAddressMap">
SELECT
u.id AS user_id,
u.name AS user_name,
u.email AS user_email,
a.id AS addr_id,
a.street,
a.city,
a.country
FROM users u
LEFT JOIN addresses a ON u.address_id = a.id
WHERE u.id = #{id}
</select>
association元素定义了User对象与Address对象之间的一对一关联关系。
6.4 一对多关联
当需要映射一对多关联关系时,可以使用collection元素。
// User类
public class User {
private int id;
private String name;
private String email;
private List<Order> orders;
// getter和setter方法
}
// Order类
public class Order {
private int id;
private Date createTime;
private BigDecimal amount;
// getter和setter方法
}
// XML映射
<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="user_id" />
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="createTime" column="create_time"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="getUserWithOrders" parameterType="int" resultMap="userWithOrdersMap">
SELECT
u.id AS user_id,
u.name AS user_name,
u.email AS user_email,
o.id AS order_id,
o.create_time,
o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
collection元素定义了User对象与Order对象之间的一对多关联关系。
6.5 鉴别器映射
当需要根据某个列的值进行不同的映射时,可以使用discriminator元素。
// Vehicle类(抽象类)
public abstract class Vehicle {
private int id;
private String brand;
// getter和setter方法
}
// Car类(继承Vehicle)
public class Car extends Vehicle {
private int doors;
// getter和setter方法
}
// Truck类(继承Vehicle)
public class Truck extends Vehicle {
private double cargoCapacity;
// getter和setter方法
}
// XML映射
<resultMap id="vehicleResultMap" type="Vehicle">
<id property="id" column="id" />
<result property="brand" column="brand"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultType="Car">
<result property="doors" column="doors"/>
</case>
<case value="2" resultType="Truck">
<result property="cargoCapacity" column="cargo_capacity"/>
</case>
</discriminator>
</resultMap>
<select id="getVehicleById" parameterType="int" resultMap="vehicleResultMap">
SELECT
id,
brand,
vehicle_type,
doors,
cargo_capacity
FROM vehicles
WHERE id = #{id}
</select>
discriminator元素根据vehicle_type列的值,决定是映射为Car对象还是Truck对象。
6.6 延迟加载
MyBatis支持延迟加载(懒加载)关联对象,即只有在真正使用关联对象时才会去查询数据库。
<!-- 开启延迟加载 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 一对一关联的延迟加载 -->
<resultMap id="userResultMap" type="User">
<id property="id" column="id" />
<result property="name" column="name"/>
<result property="email" column="email"/>
<association property="address" javaType="Address"
select="getAddressById" column="address_id"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="userResultMap">
SELECT id, name, email, address_id
FROM users WHERE id = #{id}
</select>
<select id="getAddressById" parameterType="int" resultType="Address">
SELECT id, street, city, country
FROM addresses WHERE id = #{id}
</select>
<!-- 一对多关联的延迟加载 -->
<resultMap id="userResultMap" type="User">
<id property="id" column="id" />
<result property="name" column="name"/>
<result property="email" column="email"/>
<collection property="orders" ofType="Order"
select="getOrdersByUserId" column="id"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="userResultMap">
SELECT id, name, email
FROM users WHERE id = #{id}
</select>
<select id="getOrdersByUserId" parameterType="int" resultType="Order">
SELECT id, create_time, amount
FROM orders WHERE user_id = #{id}
</select>
在这个例子中,当查询User对象时,不会立即查询Address对象或Order列表,只有在代码中实际访问这些属性时才会触发查询。
7. 动态SQL
MyBatis的动态SQL功能允许我们根据不同的条件生成不同的SQL语句。
7.1 if元素
if元素用于根据条件判断是否包含某部分SQL。
<select id="getUsersByCondition" parameterType="User" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</select>
if元素的test属性支持OGNL表达式,可以使用逻辑运算符(&&, ||, !)、比较运算符(==, !=, <, >, <=, >=)等。
7.2 choose, when, otherwise元素
choose元素类似于Java中的switch语句,提供了多个条件中选择一个的能力。
<select id="getUsersByCondition" parameterType="User" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="name != null">
name = #{name}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</where>
</select>
在这个例子中,如果id不为null,则使用id作为条件;如果id为null但name不为null,则使用name作为条件;如果id和name都为null,则使用1=1作为条件。
7.3 where元素
where元素用于动态生成WHERE子句,它会自动处理WHERE关键字以及AND/OR前缀。
<select id="getUsersByCondition" parameterType="User" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
where元素会自动去除开头的AND/OR关键字,如果所有条件都不满足,则不会生成WHERE子句。
7.4 set元素
set元素用于动态生成SET子句,它会自动处理SET关键字以及逗号后缀。
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null">
name = #{name},
</if>
<if test="email != null">
email = #{email},
</if>
</set>
WHERE id = #{id}
</update>
set元素会自动去除结尾的逗号,如果所有条件都不满足,则不会生成SET子句。
7.5 foreach元素
foreach元素用于遍历集合,生成重复的SQL片段。
<select id="getUsersByIds" parameterType="list" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
foreach元素的属性:
- collection:要遍历的集合,可以是array、list、map等。
- item:集合中的元素。
- index:集合的索引。
- open:开始字符。
- separator:分隔符。
- close:结束字符。
7.6 bind元素
bind元素用于创建一个变量,将OGNL表达式的值绑定到上下文中。
<select id="getUsersByName" parameterType="string" resultType="User">
<bind name="pattern" value="'%' + name + '%'" />
SELECT * FROM users
WHERE name LIKE #{pattern}
</select>
bind元素创建了一个名为pattern的变量,值为’%’ + name + ‘%’,这样就可以在SQL中使用这个变量。
7.7 trim元素
trim元素用于自定义SQL片段的前缀和后缀。
<select id="getUsersByCondition" parameterType="User" resultType="User">
SELECT * FROM users
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</trim>
</select>
trim元素的属性:
- prefix:要添加的前缀。
- prefixOverrides:要去除的前缀。
- suffix:要添加的后缀。
- suffixOverrides:要去除的后缀。
7.8 sql元素和include元素
sql元素用于定义可重用的SQL片段,include元素用于引用这些片段。
<sql id="userColumns">id, name, email</sql>
<select id="getUserById" parameterType="int" resultType="User">
SELECT <include refid="userColumns"/>
FROM users WHERE id = #{id}
</select>
<select id="getAllUsers" resultType="User">
SELECT <include refid="userColumns"/>
FROM users
</select>
使用sql元素和include元素可以避免重复编写SQL片段,提高代码的可维护性。
8. 缓存机制
MyBatis提供了一级缓存和二级缓存,用于提高查询性能。
8.1 一级缓存
一级缓存是SqlSession级别的缓存,即在同一个SqlSession中,相同的查询只会执行一次,后续查询会直接从缓存中获取结果。
// 使用一级缓存
SqlSession session = sqlSessionFactory.openSession();
try {
// 第一次查询,会查询数据库
User user1 = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
// 第二次查询相同的语句和参数,会直接从缓存返回结果
User user2 = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
// user1和user2是同一个对象
System.out.println(user1 == user2); // 输出true
} finally {
session.close();
}
一级缓存的生命周期与SqlSession相同,当SqlSession关闭、提交或回滚事务、执行更新语句时,缓存会被清空。
8.2 一级缓存的配置
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>
当localCacheScope设置为STATEMENT时,每次查询结束后缓存都会被清空,相当于禁用了一级缓存。
8.3 二级缓存
二级缓存是命名空间级别的缓存,即在同一个命名空间(Mapper)中,不同的SqlSession可以共享缓存。
<!-- 在映射文件中开启二级缓存 -->
<mapper namespace="org.mybatis.example.UserMapper">
<cache/>
<!-- 查询语句 -->
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
要使用二级缓存,还需要在配置文件中启用缓存,并确保结果对象是可序列化的。
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
// User类需要实现Serializable接口
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private String email;
// getter和setter方法
}
8.4 二级缓存的配置
可以通过cache元素的属性配置二级缓存的行为。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
cache元素的属性:
- eviction:缓存回收策略(LRU、FIFO、SOFT、WEAK)。
- flushInterval:刷新间隔,单位毫秒。
- size:缓存引用数量。
- readOnly:是否只读。
- blocking:是否阻塞。
- type:自定义缓存实现类。
8.5 自定义缓存
MyBatis允许我们使用自定义的缓存实现。
<cache type="org.mybatis.example.CustomCache">
<property name="host" value="localhost"/>
<property name="port" value="6379"/>
</cache>
自定义缓存类需要实现Cache接口。
public class CustomCache implements Cache {
private final String id;
private String host;
private int port;
public CustomCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
// 其他Cache接口方法实现
// ...
// 属性设置方法
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
}
8.6 缓存的使用场景与注意事项
缓存适用于以下场景:
- 查询频率高,变更频率低的数据。
- 不需要实时数据的查询。
- 重复查询相同数据的场景。
缓存使用注意事项:
- 在多表查询时要谨慎使用缓存,因为任何一个表的更新都会导致缓存失效。
- 在分布式环境下,要确保缓存的一致性,可能需要使用分布式缓存。
- 对于敏感数据,应该禁用缓存或使用加密缓存。
9. 插件机制
MyBatis的插件机制允许我们拦截MyBatis的执行,修改或增强其行为。
9.1 拦截器接口
MyBatis的插件需要实现Interceptor接口。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
- intercept方法:拦截方法,在目标方法执行时被调用。
- plugin方法:生成代理对象,决定是否拦截目标对象。
- setProperties方法:设置属性,在初始化插件时被调用。
9.2 @Intercepts注解
@Intercepts注解用于指定要拦截的方法。
@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 ExamplePlugin implements Interceptor {
// 插件实现
}
@Intercepts注解包含一个或多个@Signature注解,每个@Signature注解指定要拦截的方法签名,包括类型(type)、方法名(method)和参数类型(args)。
9.3 可拦截的方法
MyBatis允许拦截以下方法:
- Executor: update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
- ParameterHandler: getParameterObject, setParameters
- ResultSetHandler: handleResultSets, handleOutputParameters
- StatementHandler: prepare, parameterize, batch, update, query
// 拦截Executor.update方法的示例
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class ExecutorUpdateInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取目标对象
Executor executor = (Executor) invocation.getTarget();
// 获取方法参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 在执行前处理
System.out.println("Before update: " + ms.getId());
// 执行原方法
Object result = invocation.proceed();
// 在执行后处理
System.out.println("After update: " + ms.getId());
return result;
}
@Override
public Object plugin(Object target) {
// 判断目标对象是否是要拦截的类型
if (target instanceof Executor) {
// 返回代理对象
return Plugin.wrap(target, this);
}
// 返回原对象
return target;
}
@Override
public void setProperties(Properties properties) {
// 设置属性
}
}
9.4 插件的配置
插件需要在MyBatis配置文件中注册。
<plugins>
<plugin interceptor="org.mybatis.example.ExecutorUpdateInterceptor">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
插件的配置包括拦截器类名和插件属性。
9.5 插件的应用场景
插件的常见应用场景包括:
- 性能监控:记录SQL执行时间,检测慢查询。
- 分页功能:自动处理分页参数。
- 数据加密:自动加密敏感数据。
- 审计日志:记录数据变更操作。
- 动态数据源:根据不同条件切换数据源。
// 性能监控插件示例
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class PerformanceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
System.out.println("SQL执行时间: " + (endTime - startTime) + "ms, ID: " + ms.getId());
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
10. 与Spring集成
MyBatis可以与Spring框架集成,简化配置和使用。
10.1 依赖配置
要使用MyBatis-Spring,需要添加相关依赖。
<!-- Maven依赖 -->
<dependencies>
<!-- Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.9</version>
</dependency>
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!-- MyBatis-Spring依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
</dependencies>
10.2 Spring配置
配置MyBatis-Spring主要包括数据源、SqlSessionFactory、Mapper扫描等。
<!-- Spring配置文件 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd">
<!-- 扫描组件 -->
<context:component-scan base-package="org.mybatis.example"/>
<!-- 引入属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:org/mybatis/example/mapper/*.xml"/>
</bean>
<!-- 配置MapperScannerConfigurer -->
<mybatis:scan base-package="org.mybatis.example.mapper"/>
</beans>
10.3 Java配置
如果使用Java配置(无XML),可以使用@Configuration注解配置MyBatis-Spring。
@Configuration
@MapperScan("org.mybatis.example.mapper")
public class MyBatisConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:org/mybatis/example/mapper/*.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
}
10.4 事务管理
MyBatis-Spring使用Spring的事务管理器管理事务。
<!-- Spring配置文件中的事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 启用基于注解的事务管理 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
// 使用@Transactional注解管理事务
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void updateUser(User user) {
userMapper.updateUser(user);
// 如果发生异常,事务会自动回滚
// ...
}
}
10.5 Mapper注入
MyBatis-Spring支持将Mapper注入到Spring管理的Bean中。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserById(int id) {
return userMapper.getUserById(id);
}
public List<User> getAllUsers() {
return userMapper.getAllUsers();
}
public void insertUser(User user) {
userMapper.insertUser(user);
}
public void updateUser(User user) {
userMapper.updateUser(user);
}
public void deleteUser(int id) {
userMapper.deleteUser(id);
}
}
在Service层注入Mapper接口,无需手动创建SqlSession,MyBatis-Spring会自动处理。
11. 深入理解SqlSession
SqlSession是MyBatis的核心接口,深入理解它的工作原理有助于我们更好地使用MyBatis。
11.1 SqlSession的创建
SqlSession的创建过程如下:
// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建SqlSessionFactoryBuilder
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 3. 创建SqlSessionFactory
SqlSessionFactory factory = builder.build(inputStream);
// 4. 创建SqlSession
SqlSession session = factory.openSession();
在内部,SqlSessionFactory会创建一个Transaction对象,然后基于Transaction和配置的ExecutorType创建一个Executor对象,最后基于Executor创建SqlSession。
11.2 SqlSession的方法
SqlSession接口提供了许多方法,可以分为几类:
查询方法
// 查询单个对象
User user = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
// 查询列表
List<User> users = session.selectList("org.mybatis.example.UserMapper.getAllUsers");
// 查询映射
Map<Integer, User> userMap = session.selectMap("org.mybatis.example.UserMapper.getAllUsers", "id");
// 查询游标
Cursor<User> userCursor = session.selectCursor("org.mybatis.example.UserMapper.getAllUsers");
更新方法
// 插入
int insertCount = session.insert("org.mybatis.example.UserMapper.insertUser", user);
// 更新
int updateCount = session.update("org.mybatis.example.UserMapper.updateUser", user);
// 删除
int deleteCount = session.delete("org.mybatis.example.UserMapper.deleteUser", 1);
事务方法
// 提交事务
session.commit();
// 回滚事务
session.rollback();
// 关闭会话
session.close();
Mapper方法
// 获取Mapper接口的代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
11.3 SqlSession的生命周期
SqlSession的生命周期应该是请求作用域的,即一个请求一个SqlSession,请求结束后关闭SqlSession。
SqlSession session = sqlSessionFactory.openSession();
try {
// 使用SqlSession
// ...
// 提交事务
session.commit();
} catch (Exception e) {
// 回滚事务
session.rollback();
throw e;
} finally {
// 关闭SqlSession
session.close();
}
在Spring环境中,SqlSession的生命周期由Spring管理,无需手动创建和关闭。
11.4 SqlSession与线程安全
SqlSession不是线程安全的,不能在多个线程间共享同一个SqlSession实例。每个线程应该有自己的SqlSession实例。
// 错误示例:多个线程共享同一个SqlSession
SqlSession session = sqlSessionFactory.openSession();
// 线程1使用session
// 线程2也使用session
// 可能导致不可预期的问题
// 正确示例:每个线程有自己的SqlSession
// 线程1
SqlSession session1 = sqlSessionFactory.openSession();
try {
// 使用session1
} finally {
session1.close();
}
// 线程2
SqlSession session2 = sqlSessionFactory.openSession();
try {
// 使用session2
} finally {
session2.close();
}
在Spring环境中,SqlSession是绑定到当前线程的,因此是线程安全的。
12. 常见问题与解决方案
这一节我们将介绍MyBatis使用过程中的常见问题和解决方案。
12.1 参数绑定问题
问题:单个参数时无法识别参数名
// Mapper接口
User getUserById(int userId);
// XML映射
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{userId} <!-- 这里无法识别userId -->
</select>
解决方案:
- 使用任意名称,如#{value}、#{id}或#{任意名称}
- 使用@Param注解指定参数名
// 使用@Param注解
User getUserById(@Param("userId") int userId);
// XML映射
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{userId} <!-- 这里可以识别userId -->
</select>
问题:多个参数时无法识别参数名
// Mapper接口
List<User> getUsersByNameAndEmail(String name, String email);
// XML映射
<select id="getUsersByNameAndEmail" resultType="User">
SELECT * FROM users WHERE name = #{name} AND email = #{email} <!-- 这里无法识别name和email -->
</select>
解决方案:
- 使用@Param注解指定参数名
- 使用Map传递多个参数
- 使用JavaBean对象传递多个参数
// 使用@Param注解
List<User> getUsersByNameAndEmail(@Param("name") String name, @Param("email") String email);
// 使用Map传递多个参数
List<User> getUsersByCondition(Map<String, Object> condition);
// 使用JavaBean对象传递多个参数
List<User> getUsersByCondition(User user);
12.2 结果映射问题
问题:列名与属性名不一致
// User类
public class User {
private int id;
private String userName; // 与数据库列名不一致
private String userEmail; // 与数据库列名不一致
// getter和setter方法
}
// 数据库表
// users表:id, name, email
解决方案:
- 使用别名
- 使用resultMap
- 启用驼峰命名自动映射
<!-- 使用别名 -->
<select id="getUserById" resultType="User">
SELECT
id AS id,
name AS userName,
email AS userEmail
FROM users WHERE id = #{id}
</select>
<!-- 使用resultMap -->
<resultMap id="userResultMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="name"/>
<result property="userEmail" column="email"/>
</resultMap>
<select id="getUserById" resultMap="userResultMap">
SELECT id, name, email
FROM users WHERE id = #{id}
</select>
<!-- 启用驼峰命名自动映射 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
12.3 性能问题
问题:查询结果集过大
解决方案:
- 分页查询
- 使用游标
- 优化SQL查询条件
// 分页查询
List<User> getUsersByPage(@Param("offset") int offset, @Param("limit") int limit);
// 使用游标
Cursor<User> getAllUsersCursor();
<!-- 分页查询 -->
<select id="getUsersByPage" resultType="User">
SELECT * FROM users LIMIT #{offset}, #{limit}
</select>
// 使用游标示例
try (Cursor<User> cursor = userMapper.getAllUsersCursor()) {
for (User user : cursor) {
// 处理用户
}
}
问题:频繁创建SqlSession
解决方案:
- 使用SqlSessionTemplate(Spring环境)
- 使用ThreadLocal管理SqlSession
// 使用SqlSessionTemplate
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
// 注入并使用SqlSessionTemplate
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
public User getUserById(int id) {
return sqlSessionTemplate.selectOne("org.mybatis.example.UserMapper.getUserById", id);
}
12.4 配置问题
问题:配置文件路径错误
解决方案:
- 检查配置文件路径
- 确保配置文件在classpath中
- 使用绝对路径
// 使用相对路径
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 使用绝对路径
InputStream inputStream = new FileInputStream("/path/to/mybatis-config.xml");
问题:映射文件未注册
解决方案:
- 在配置文件中注册映射文件
- 使用包扫描注册映射文件
- 检查映射文件的namespace是否正确
<!-- 注册单个映射文件 -->
<mappers>
<mapper resource="org/mybatis/example/UserMapper.xml"/>
</mappers>
<!-- 使用包扫描注册映射文件 -->
<mappers>
<package name="org.mybatis.example"/>
</mappers>
12.5 动态SQL问题
问题:动态SQL条件不生效
解决方案:
- 检查变量名是否正确
- 检查OGNL表达式是否正确
- 使用where元素替代where 1=1
<!-- 正确示例 -->
<select id="getUsersByCondition" parameterType="User" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
问题:foreach元素使用错误
解决方案:
- 检查collection属性是否正确
- 检查item属性是否正确
- 对于单个参数使用@Param注解
<!-- 正确示例 -->
<select id="getUsersByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
// Mapper接口
List<User> getUsersByIds(@Param("list") List<Integer> ids);
13. 最佳实践
最后一节,我们总结一些MyBatis的最佳实践,帮助大家更好地使用MyBatis。
13.1 项目结构
推荐的项目结构:
src/main/java
├── org.example.domain // 领域对象(实体类)
├── org.example.mapper // Mapper接口
├── org.example.service // 服务层
└── org.example.controller // 控制器层
src/main/resources
├── org.example.mapper // Mapper XML文件
├── mybatis-config.xml // MyBatis配置文件
└── application.properties // 应用配置文件
13.2 命名规范
- 表名:使用下划线分隔单词,如user_info。
- 列名:使用下划线分隔单词,如user_name。
- Java类名:使用驼峰命名法,如UserInfo。
- 属性名:使用驼峰命名法,如userName。
- Mapper接口:使用XxxMapper命名,如UserMapper。
- XML文件:与Mapper接口同名,如UserMapper.xml。
- 方法名:使用动词+名词,如getUserById。
13.3 使用接口绑定
推荐使用接口绑定方式,而不是使用字符串调用SQL语句。
// 推荐:使用接口绑定
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
// 不推荐:使用字符串调用
User user = session.selectOne("org.example.mapper.UserMapper.getUserById", 1);
接口绑定的优点:
- 类型安全
- 编译时检查
- 代码可读性更好
- IDE支持更好(代码补全、重构等)
13.4 参数处理
- 单个参数:可以不使用@Param注解
- 多个参数:使用@Param注解为参数命名
- 复杂对象:使用JavaBean传递多个相关参数
// 单个参数
User getUserById(int id);
// 多个参数
List<User> getUsersByNameAndEmail(@Param("name") String name, @Param("email") String email);
// 复杂对象
void insertUser(User user);
13.5 结果映射
- 简单映射:使用resultType
- 复杂映射:使用resultMap
- 启用驼峰命名自动映射
<!-- 简单映射 -->
<select id="getUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- 复杂映射 -->
<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="user_id" />
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<!-- 启用驼峰命名自动映射 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
13.6 动态SQL
- 使用if元素进行条件判断
- 使用where元素替代where 1=1
- 使用set元素处理更新语句
- 使用foreach元素处理集合
- 使用sql元素重用SQL片段
<!-- 动态查询 -->
<select id="getUsersByCondition" parameterType="User" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
<!-- 动态更新 -->
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null">
name = #{name},
</if>
<if test="email != null">
email = #{email},
</if>
</set>
WHERE id = #{id}
</update>
13.7 缓存使用
- 慎用二级缓存,特别是在多表查询时
- 对于读多写少的数据,可以使用缓存
- 对于实时性要求高的数据,不要使用缓存
<!-- 配置二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
13.8 分页查询
- 使用RowBounds进行内存分页(小数据量)
- 使用LIMIT进行物理分页(大数据量)
- 使用分页插件
// 使用RowBounds进行内存分页
RowBounds rowBounds = new RowBounds(offset, limit);
List<User> users = userMapper.getAllUsers(rowBounds);
// 使用LIMIT进行物理分页
List<User> users = userMapper.getUsersByPage(offset, limit);
<!-- 使用LIMIT进行物理分页 -->
<select id="getUsersByPage" resultType="User">
SELECT * FROM users LIMIT #{offset}, #{limit}
</select>
13.9 异常处理
- 使用try-finally确保SqlSession正确关闭
- 在Spring环境中,让Spring管理SqlSession
- 使用日志记录SQL异常
// 使用try-finally确保SqlSession正确关闭
SqlSession session = sqlSessionFactory.openSession();
try {
// 使用SqlSession
// ...
// 提交事务
session.commit();
} catch (Exception e) {
// 回滚事务
session.rollback();
// 记录异常
logger.error("SQL执行异常", e);
throw e;
} finally {
// 关闭SqlSession
session.close();
}
13.10 性能优化
- 合理使用缓存
- 优化SQL语句
- 使用批量操作
- 延迟加载
- 使用游标处理大结果集
<!-- 批量插入 -->
<insert id="batchInsertUsers" parameterType="list">
INSERT INTO users (name, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
</insert>
<!-- 启用延迟加载 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
13.11 与Spring集成
- 使用@Mapper注解标记Mapper接口
- 使用@MapperScan注解扫描Mapper接口
- 使用@Transactional注解管理事务
- 使用SqlSessionTemplate替代SqlSession
// 使用@Mapper注解
@Mapper
public interface UserMapper {
User getUserById(int id);
// 其他方法
}
// 使用@MapperScan注解
@Configuration
@MapperScan("org.example.mapper")
public class MyBatisConfig {
// 配置
}
// 使用@Transactional注解
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void updateUser(User user) {
userMapper.updateUser(user);
}
}
13.12 测试
- 使用MyBatis-Spring-Test进行集成测试
- 使用H2等内存数据库进行单元测试
- 使用DbUnit准备测试数据
// 使用MyBatis-Spring-Test进行测试
@RunWith(SpringRunner.class)
@MybatisTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testGetUserById() {
User user = userMapper.getUserById(1);
assertNotNull(user);
assertEquals("Tom", user.getName());
}
}
总结
本文详细介绍了MyBatis的工作原理,包括核心组件、工作流程、配置文件、参数处理、结果映射、动态SQL、缓存机制、插件机制、与Spring集成等方面。
MyBatis是一个优秀的持久层框架,它提供了灵活的SQL映射和丰富的特性,使数据库操作变得简单而高效。通过深入理解MyBatis的工作原理,我们可以更好地使用这个框架,编写出高质量的数据访问层代码。