写在前面
首先向各位大大问一声新年好,今年春节由于疫情本人也没有回家,趁着这几天,把 MyBatis 的源码基本看完了。MyBatis 的源码相对于 Spring 来说更为简单,因此并没有花费太多的时间。本篇作为 MyBatis 的入门篇,主要介绍一些 MyBatis 的基本用法,更为详细的使用建议参考 MyBatis 官网,本文会将侧重点放在理解 MyBatis,相对于官网,各位读者会有不一样的收获。后面会出一系列的 MyBatis 文章,带你深层次的分析其内部原理,敬请期待。
背景
自 JDBC 规范诞生,Java 程序员有了统一的方式操作关系型数据库,这大大降低了 Java 程序员学习操作不同数据库产品的成本。回顾前篇文章《Java 基础知识之 JDBC》 ,每次操作数据库都要进行如下的操作:注册驱动、获取连接、执行 SQL 语句、将结果集中的数据转换为 Java 实体对象、释放连接。这些步骤期间还要处理各种异常,过程不仅繁琐,而且产生了大量模板式代码。
为了进一步简化对数据库的操作,ORM (Object Relation Mapping) 框架应运诞生。ORM 框架将关系型数据库中的数据和 Java 实体类相互映射,从而可以以面向对象的方式操作数据库,而无需对数据库相关知识有深入了解。
ORM 框架最初的代表是 Hibernate,使用 Hibernate 只需要在映射文件中指定数据库表和实体类的映射关系即可,而不必手工编写 SQL,然而正是由于其全表映射导致执行的 SQL 不够灵活,如无法只查询个别字段、不支持动态表名、不支持存储过程,最严重的是难以对 SQL 进行优化,这导致 Hibernate 在面对海量数据时的性能极差。
为了解决 Hibernate 的不足,我们的主角 MyBatis 就诞生了。MyBatis 是一个半自动化的 ORM 框架,这是因为使用 MyBatis 不仅要指定数据库表和实体类的映射规则,还要指定操作数据库的 SQL,这也让我们操作数据库更为灵活。只要使用 JDBC 能进行的操作,都可以使用 MyBatis 进行操作。设置参数、获取结果集、转换为实体类这些操作 MyBatis 内部都已实现,而无需用户关心。
使用 MyBatis 操作数据库
了解一个开源框架,应由表及里进行,先了解其提供的功能及基本使用方式,然后再了解其底层实现, 我们已经知道了 Mybatis 主要用来简化数据库操作,并支持 Java 对象和数据库表记录的映射,下面通过一个简单的案例了解 MyBatis 的基本使用。
假设我们有一个用户表,定义如下。
CREATE TABLE `user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
`password` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
插入一条记录。
INSERT INTO test.`user` (username,password,create_time) VALUES ('hkp','123456',now());
数据库表对应的实体类如下。
public class User {
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 创建时间
*/
private Date createTime;
// 省略 getter/setter/toString 方法
}
我们想要根据用户名查询用户的信息,使用 MyBatis 的方式如下。
MyBatis 引入
首先需要引入 MyBatis ,MyBatis 的 maven 坐标如下。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
当然,数据库的驱动也不能忘记,我们使用的是 MySQL,驱动的坐标如下。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
MyBatis XML 映射文件创建
使用 Mybatis 需要通过 xml 或者注解的方式指定数据库表和 Java 实体类的映射关系以及操作数据库的 SQL,由于 SQL 的复杂性,使用 xml 方式配置映射规则来说更为灵活,并且还能支持动态 SQL,日常开发中每个数据库表和实体类都有一个对应的映射文件。
classpath 下创建用户表 user 和实体类 User 之间的映射文件 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="com.zzuhkp.blog.mybatis.UserMapper">
<resultMap id="UserResultMap" type="com.zzuhkp.blog.mybatis.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="getByUsername" resultMap="UserResultMap" parameterType="string">
select *
from user
where username = #{username}
</select>
</mapper>
这个映射文件相对来说比较简单,其中根节点 mapper 配置一个命名空间,这个命名空间在早期的 MyBatis 来说并不是必选的,当前一般要配置为一个和 mapper 映射文件相对应的 Mapper 接口。
我们还配置了一个 resultMap 节点,这个节点是用来表示数据库表和哪一个实体类之间的映射关系,我们为 resultMap 指定了一个 id 标识,并且指定了这个 resultMap 是用来映射 User 实体类的,接下来的子节点就是用来配置数据库表的列和 Java 实体类的属性之间的关系了。id 用来唯一标识 User 类,result 表示普通的属性。
再后面我们配置了一个 select 语句,使用 id 属性指定了这个语句的标识,MyBatis 是通过命名空间+语句标识来唯一确定语句的,这样不同的命名空间就可以具有相同的语句标识。select 不仅指定了查询参数的类型为字符串,还配置了使用 UserResultMap 作为映射,MyBatis 查询 SQL 语句后会根据这个映射关系将数据库表的记录转换为 User 对象。select 的内容就是具体的 SQL 语句了,使用这种方式还可以将 SQL 语句和 Java 代码进行隔离,SQL 语句中需要留意的是#{username}
,这是 MyBatis 设置参数的一种方式,在执行 SQL 时,会把这个参数替换为?
,预编译后设置具体的参数。语句的 id 同时也是和接口中的方法名保持一致的,使用这种方式,可以避免 MyBatis 早期通过字符串指定要查询的语句这种硬编码问题,使用字符串很容易拼写错误,而利用 Java 代码直接调用方法的方式来说一般则不会出问题。
和映射文件对应的 Java Mapper 接口如下。
package com.zzuhkp.blog.mybatis;
import java.util.List;
/**
* @author zzuhkp
* @date 2021/2/15 9:47 下午
* @since 1.0
*/
public interface UserMapper {
User getByUsername(String username);
}
可以看到,接口的全路径名和 mapper xml 文件的命名空间是一致的,方法名和 select 节点的 id 保持一致,参数类型和 select 节点指定的参数类型保持一致,返回值类型和 select 节点指定的 resultMap 的类型保持一致。
MyBatis 配置文件创建
每个数据库表一般都有一个对应的 Mapper XML 文件及操作表的 Java 接口,而配置只有一份,这个配置是全局性的。事实上 MyBatis 的配置文件并不是必须的,MyBatis 的配置文件对应了一个 Java 类 Configuration。我们直接使用这个配置类也可以,例如在 spring-boot 项目中,已经省略了这个配置文件。配置中具有大量全局性的参数,必须指定的是使用的数据源、事务管理器。正常来说,我们也会在配置中指定映射文件,如果没有映射文件,使用 MyBatis 将毫无意义。在当前案例中,我们的配置文件内容如下。
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- autoMappingBehavior should be set in each test case -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
</transactionManager>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="12345678"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
上面的配置也是一个最简化的配置,我们设置了默认使用的 environment 是 development,并且配置了一个 id 为 development 的 environment ,可以猜到, MyBatis 中是可以配置多个 environment 的,使用配置时如果没有指定 environment 就会使用默认的。environment 中包含事务管理器和数据源的相关配置,这是连接数据库所必须的。最后我们还配置了 UserMapper。
数据库查询
上面的映射及配置都是一些准备工作,完成后我们就可以进行真正的开发工作了。使用 MyBatis 根据用户名查询用户的代码如下。
public class App {
public static void main(String[] args) throws IOException {
// 利用 MyBatis 工具类创建配置文件对应的 InputStream
InputStream configInputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建 SqlSessionFactory 的构建器
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
// 构建 SqlSession 工厂
SqlSessionFactory factory = factoryBuilder.build(configInputStream);
// 获取 SqlSession
SqlSession sqlSession = factory.openSession();
// 执行查询
// MyBatis 老式的查询方式
// User user = sqlSession.selectOne("com.zzuhkp.blog.mybatis.UserMapper.getByUsername", "hkp");
// 获取映射器实例
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行映射器方法
User user = userMapper.getByUsername("hkp");
// 关闭会话
sqlSession.close();
System.out.println(user);
}
}
数据库查询操作的发起是 SqlSession 或根据 SqlSession 获取的 Mapper,SqlSession 使用完毕后需要关闭,Mapper 作为一个接口,MyBatis 返回给我们的是一个代理对象,这个代理对象的查询操作和 SqlSession 内部保持一致。上述代码中还涉及了 SqlSessionFactory、SqlSessionFactoryBuilder,这几个类是使用 MyBatis 所必须了解的。
上述示例代码的项目结构如下。
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── zzuhkp
│ │ └── blog
│ │ └── mybatis
│ │ ├── App.java
│ │ ├── User.java
│ │ └── UserMapper.java
│ └── resources
│ ├── UserMapper.xml
│ └── mybatis-config.xml
Mapper
Mapper 即映射器,MyBatis 的 Mapper 包括 xml 文件及 Java 接口。
早期的 mapper 只有 xml 文件,xml 文件中配置数据库表的列和 Java 属性的映射关系、SQL 语句。映射关系不仅支持简单的列和属性映射,还支持复杂的属性。使用 xml 文件可以更方便的编写动态 SQL。每个查询语句都有自己的标识,在 SqlSession 中传入这个标识及参数操作数据库,返回操作结果。
由于使用字符串表示语句标识很容易出错,因此后面又提出了和 xml 对应的 Java 接口,xml 映射文件中的命名空间就是接口的全限定名,xml 中语句的 id 就是方法名,将 xml 中的语句映射到接口方法中,通过生成接口的代理,这样就可以利用 Java 代码的方式操作数据库,而不容易出错。关于代理,不熟悉的小伙伴可参考我前面的文章《Java 中创建代理的几种方式》。
旧的操作方式如下。
User user = sqlSession.selectOne("com.zzuhkp.blog.mybatis.UserMapper.getByUsername", "hkp");
新的操作方式如下。
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getByUsername("hkp");
Java 5 开始注解诞生以后,MyBatis 又对注解加以支持,在 Mapper 接口的方法上可以添加和 xml 标签对应的注解,这样就可以省略 xml 文件,但是由于 SQL 的复杂性,使用注解不够灵活,因此目前注解使用较少。上面的例子改为注解的方式如下。
public interface UserMapper {
@Select("select * from user where username = #{username}")
User getByUsername(String username);
}
SqlSession
SqlSession 是 MyBatis 早期不存在 Mapper 接口时操作数据库的核心接口,类似 JDBC 中的 Connection,除了增删改查,它还提供了管理事务的方法。SqlSession 的默认实现是 DefaultSqlSession ,这个接口中的方法如下。由于其实现较为复杂,将在后面的文章单独进行分析。
public interface SqlSession extends Closeable {
// 查询一条记录
<T> T selectOne(String statement);
<T> T selectOne(String statement, Object parameter);
// 查询多条记录,返回列表
<E> List<E> selectList(String statement);
<E> List<E> selectList(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
// 查询多条记录,将列表转换为 map 返回
<K, V> Map<K, V> selectMap(String statement, String mapKey);
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
// 查询多条记录,可使用返回的游标迭代
<T> Cursor<T> selectCursor(String statement);
<T> Cursor<T> selectCursor(String statement, Object parameter);
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
// 查询多条记录,使用自定义的结果处理器处理返回结果
void select(String statement, Object parameter, ResultHandler handler);
void select(String statement, ResultHandler handler);
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
// 插入记录
int insert(String statement);
int insert(String statement, Object parameter);
// 更新记录
int update(String statement);
int update(String statement, Object parameter);
// 删除记录
int delete(String statement);
int delete(String statement, Object parameter);
// 刷新批量操作的语句
List<BatchResult> flushStatements();
// 提交事务
void commit();
void commit(boolean force);
// 回滚事务
void rollback();
void rollback(boolean force);
// 清除 session 中的缓存
void clearCache();
// 获取 MyBatis 当前使用的配置
Configuration getConfiguration();
// 获取数据库连接
Connection getConnection();
// 获取 Mapper 接口的实例
<T> T getMapper(Class<T> type);
}
SqlSessionFactory
SqlSession 的默认实现是 DefaultSqlSession ,但是我们一般不会直接实例化 DefaultSqlSession,而是使用 SqlSessionFactory 创建 SqlSession,SqlSessionFactory 会将其持有的 Configuration 传递到 SqlSession,这样 SqlSession 就可以执行 SQL。SqlSessionFactory 是工厂模式的典型实现,使用这种方式可以根据不同的条件创建不同的 SqlSession,SqlSessionFactory 的默认实现是 DefaultSqlSessionFactory。下面是 SqlSessionFactory 中的一些方法。
public interface SqlSessionFactory {
// 创建 SqlSession
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
// 获取当前 MyBatis 的配置
Configuration getConfiguration();
}
简单分析 SqlSessionFactory 的实现 DefaultSqlSessionFactory 如下。
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
/**
* 根据 DataSource 创建 SqlSession
*
* @param execType 执行器类型
* @param level 事务隔离级别
* @param autoCommit 是否自动提交事务
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 从配置中获取当前环境
final Environment environment = configuration.getEnvironment();
// 获取事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建事务对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建执行器对象
final Executor executor = configuration.newExecutor(tx, execType);
// 创建 SqlSession 对象并返回
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
// 获取事务工厂
private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
return environment.getTransactionFactory();
}
}
DefaultSqlSessionFactory 持有配置对象,先根据配置创建执行器,然后创建 DefaultSqlSession 。执行器 Executor 是 MyBatis 内部用来执行 SQL 的入口,后面的文章会进行分析。
SqlSessionFactoryBuilder
SqlSessionFactory 能够创建 SqlSession 最重要的原因就是它持有 MyBatis 的配置,然而 MyBatis 的配置由于其复杂性,直接实例 Configuration 进行配置会比较复杂,因此 MyBatis 又抽象出来一个 SqlSessionFactoryBuilder 类,用来解析 xml 的配置,构建 SqlSessionFactory。SqlSesssionFactoryBuilder 中有很多重载的 #build 方法,下面简单通过源码分析上述查询用户示例中我们构建 SqlSessionFactory 的代码。
public class SqlSessionFactoryBuilder {
// 构建 SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
// 构建 SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 解析 xml 配置文件为 Configuration
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
// 构建 SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
可以看到,SqlSessionFactoryBuilder 本质就是解析 xml 配置文件为 Configuration,然后实例化 DefaultSqlSessionFactory 对象,它主要简化了我们创建 Configuration 的工作。
总结
本篇作为 MyBatis 的入门文章,主要介绍了 MyBatis 诞生的背景,并通过一个案例演示了 MyBatis 的使用,并着重对 Mapper、SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder 进行了介绍。这些都是站在使用方的角度进行介绍,作为开胃菜,通过本篇对 MyBatis 有了简单了解之后,后面就可以着手分析 MyBatis 的源码。