简介
Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository 层中解耦出来,除了这些基本功能外,它还提供了动态 sql、延迟加载、缓存等功能。 相比 Hibernate,Mybatis 更面向数据库,可以灵活地对 sql 语句进行优化。
针对 Mybatis 的分析,我会拆分成使用、配置、源码、生成器等部分,都放在 Mybatis 这个系列里,内容将持续更新。本文是这个系列的第一篇文章,将从以下两个问题展开 :
-
持久层框架解决了哪些问题?
-
如何使用 Mybatis(这里会从入门到深入)?
项目环境的说明
为了更好地分析 Mybatis 的特性,本项目不会引入任何的依赖注入框架,将使用比较原生态的方式来使用 Mybatis。
工程环境
JDK:1.8.0_231
maven:3.6.1
IDE:Spring Tool Suites4 for Eclipse 4.12 (装有 Mybatipse 插件)
mysql:5.7.28
依赖引入
Mybatis 有自带的连接池,但实际项目中建议还是引入第三方的比较好。
<!-- Mybatis -->
<dependency>
<groupId>org.Mybatis</groupId>
<artifactId>Mybatis</artifactId>
<version>3.5.4</version>
</dependency>
<!-- mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!-- logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<type>jar</type>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.6.1</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
数据库脚本
在这个项目里面,我希望尽可能地模拟出实际项目的各种场景,例如,高级条件查询、关联查询(一对一关联、多对多关联和自关联),并研究在对应场景下如何使用 Mybatis 解决问题。本项目的 ER 图如下,涉及到 4 张主表和 2 张中间表,具体的 sql 脚本也提供好了([脚本路径]( https://github.com/ZhangZiSheng001/mybatis-projects /sql)):
持久层框架解决了哪些问题
在分析如何使用 mybatis 之前,我们先来研究一个问题:持久层框架解决了哪些问题?
假设没有持久层框架,首先想到的就是使用 JDBC 来操作数据库。这里我简单地引入一个需求,就是我想通过 id 查询出一个员工对象。下面仅会从repository/DAO 层的角度来考虑如何实现,所以我们不需要去考虑 service 层中事务提交和连接关闭的问题,当然,这样会遇到一个问题,就是我们必须保证 service 层的事务和持久层的是同一个,这一点会通过 **Utils 来解决,因为不是本文重点,这里不展开。
用 JDBC 方式查询一个员工
下面用 JDBC 查询员工对象。
有人可能会问,就算你不适用持久层框架,你还可以使用 DBUtils 或者自己封装 JDBC 代码啊?这里需要强调下,这种封装其实是持久层框架应该做的事,我们自己手动封装,其实已经在实现一个持久层框架了。所以,为了暴露纯粹的 JDBC 实现的缺点,这里尽量不去封装。
@Override
public Employee get(String id) throws SQLException {
Employee employee = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
// 创建sql
String sql = "select * from demo_employee where id = ?";
try {
// 获得连接(JDBCUtils保证同一线程获得同一个连接对象)
Connection connection = JDBCUtils.getConnection();
// 获得Statement对象
statement = connection.prepareStatement(sql);
// 设置参数
statement.setObject(1, id);
// 执行,获取结果集
resultSet = statement.executeQuery();
if(resultSet.next()) {
// 映射结果集
employee = convert(resultSet);
}
// 返回员工对象
return employee;
} finally {
// 释放资源
JDBCUtils.release(null, statement, resultSet);
}
}
/**
* <p>通过结果集构造员工对象</p>
* @author: zzs
* @date: 2020年3月28日 下午12:20:02
* @param resultSet
* @return: Employee
* @throws SQLException
*/
private Employee convert(ResultSet resultSet) throws SQLException {
Employee employee = new Employee();
employee.setId(resultSet.getString("id"));
employee.setName(resultSet.getString("name"));
employee.setGender(resultSet.getBoolean("gender"));
employee.setNo(resultSet.getString("no"));
employee.setAddress(resultSet.getString("address"));
employee.setDeleted(resultSet.getBoolean("deleted"));
employee.setDepartmentId(resultSet.getString("department_id"));
employee.setPassword(resultSet.getString("password"));
employee.setPhone(resultSet.getString("phone"));
employee.setStatus(resultSet.getByte("status"));
employee.setCreate(resultSet.getDate("gmt_create"));
employee.setModified(resultSet.getDate("gmt_modified"));
return employee;
}
通过上面的代码,我们可以看到两个主要的问题:
- 每个 Repository/DAO 方法都会出现繁琐、重复的 JDBC 代码。
- sql 和 DAO/Repository 的程序代码耦合度太高,不能统一管理。这里的 sql 包括了 sql 的定义、参数设置和结果集映射,强调一点,不是说 sql 不能出现在 java 类中,而是说应该从 DAO/Repository 的程序代码中解耦出来,进行集中管理。
说到这里,我们可以总结出来,为了项目的方便和解耦,一个基本的持久层框架需要做到:
- 对 JDBC 代码进行高级封装,为我们提供更简单的接口。
- 将 sql 从 DAO/Repository 中解耦出来。
Mybatis 作为一个优秀的持久层框架,针对以上问题提供了解决方案,下面我们再看看使用 Mybatis 如何实现上面的需求。
用 Mybatis 方式查询一个员工
还是通过查询员工的例子来说明,代码如下:
public Employee get(String id) {
return MybatisUtils.getMapper(EmployeeMapper.class).selectByPrimaryKey(id);
}
上面的代码没有出现任何的 JDBC 代码和 sql 代码,因为 Mybatis 对 JDBC 进行了高级封装,并且采用 Mapper 的注解或 xml 文件来统一管理 sql 的定义、参数设置和结果集映射。下面看下 xml 文件的方式:
<!-- 基础映射表 -->
<resultMap id="BaseResultMap" type="cn.zzs.mybatis.entity.Employee">
<result column="id" property="id" javaType="string" jdbcType="VARCHAR"/>
<result column="department_id" property="departmentId" javaType="string" jdbcType="VARCHAR"/>
<result column="gmt_create" property="create" javaType="date" jdbcType="TIMESTAMP"/>
<result column="gmt_modified" property="modified" javaType="date" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础字段 -->
<sql id="Base_Column_List">
e.id,
e.`name`,
e.gender,
e.no,
e.password,
e.phone,
e.address,
e.status,
e.deleted,
e.department_id,
e.gmt_create,
e.gmt_modified
</sql>
<!-- 根据id查询 -->
<select id="selectByPrimaryKey"
parameterType="java.lang.String"
resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from
demo_employee e
where
e.id = #{id}
</select>
针对 sql 解耦的问题,早期的持久层框架都偏向于将 sql 独立在配置文件中,后来才逐渐引入注解的支持,如下是Mybatis 的注解方式(EmployeeMapper 接口):
@Select("SELECT e.id, e.`name`, e.gender, e.no, e.password, e.phone, e.address, e.status, e.deleted, e.department_id, e.gmt_create, e.gmt_modified FROM demo_employee e WHERE id = #{id}")
@resultMap("BaseResultMap")
Employee selectByPrimaryKey(String id);
我认为,正如前面说到的,sql 在项目中存在形式不是重点,我们的目的是希望 sql 能被统一管理,基于这个目的实现的不同方案,都是合理的。
Mybatis 作为一款优秀的持久层框架,除了解决上面的两个基本问题,还为我们提供了懒加载、缓存、动态语句、插件等功能,下文会讲到。
补充
通过上面的内容,我们已经回答了问题:持久层框架解决了哪些问题?这里需要补充一点:
本文只是指出持久层框架的需要解决的基本问题,并没有强调必须使用 Mybatis 或 Hibernate 等通用框架。出于性能方面的考虑,部分开发者可能会采用更轻量的实现,而不是使用流行的通用框架。当然,这也是自己造轮子和使用通用轮子的区别了。
如何使用 Mybatis
本项目会模拟实际开发的各种场景来研究 Mybatis 的使用方法。在我看来,DAO 层只要有以下几个方法,已经可以满足大部分使用需求。在 DAO 层定义大量的*By*
方法是非常低级和不负责任的,然而,我接触过许多人都是这么搞的。
public interface IEmployeeRepository {
// 查询
Employee get(String id);//根据id查询
List<Employee> list(EmployeeCondition con);//根据条件查询
long count(EmployeeCondition con);//根据条件查询数量
// 删除
int delete(EmployeeCondition con);//根据条件删除
int delete(String id);//根据id删除
// 新增
int save(Employee employee);//新增
int save(List<Employee> list);//批量新增
// 更新
int update(Employee employee, EmployeeCo