大家好,我是王有志。今天开始我会和大家一起来学习在 Java 应用程序中使用非常广泛的持久层框架 MyBatis。作为 MyBatis 系列的第一篇文章,我会先对 MyBatis 以及诞生的意义做一个简单的介绍,最后我们在一起动手完成一个简单的例子。
Tips:文章最后的部分,我会对 MyBatis 中文网上“不使用 XML 构建 SqlSessionFactory”的例子进行补充。
JDBC 编程
在 Java 诞生的初期,如果想要在 Java 应用程序中访问数据库,就要使用到 JDBC(Java Data Base Connectivity)技术。
JDBC 技术是在 Java 1.1 版本中引入的,它只定义了 Java 应用程序访问数据库的接口规范,如:Connection 接口,Statement 接口和 ResultSet 接口等,而具体的实现则交由各个数据库厂商去完成。
下面我们使用 JDBC 技术完成一次对于 MySQL 数据库访问,并执行一条简单的查询语句:
// 数据库地址
private static final String URL = "jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
// 数据库用户名
private static final String USER_NAME = "root";
// 数据库密码
private static final String PASSWORD = "123456";
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取数据库链接
connection = DriverManager.getConnection(URL, USER_NAME, PASSWORD);
// 定义SQL语句
String sql = "select * from user where user_id = ?";
// 获取PreparedStatement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,序号是从1开始的
preparedStatement.setInt(1, 1);
// 查询结果集
resultSet = preparedStatement.executeQuery();
// 遍历结果集
while (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}
} catch (SQLException e) {
// 省略异常处理部分
} finally {
// 释放资源,注意处理顺序
try {
assert resultSet != null;
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
log.error("", e);
}
}
}
从上述代码中,我们可以提取出使用 JDBC 的几个步骤:
- 加载数据库驱动;
- 获取数据库链接;
- 定义 SQL 语句;
- 获取 PreparedStatement,并设置 SQL 参数;
- 通过 PreparedStatement 执行 SQL 语句,并获取结果集;
- 分析并处理结果集;
- 释放资源(ResultSet,PreparedStatement 和 Connection)
通过上述的总结可以看到,使用 JDBC 的整体流程是非常复杂的,需要操作 Connection,PreparedStatement(Statement)和 ResultSet,并且在释放资源时还需要注意释放的顺序;另外,程序中不会只执行一条 SQL 语句,如果每次执行 SQL 语句都需要加载驱动,获取链接等操作,程序中就会出现大量重复代码;最后,使用 JDBC 的过程中存在多处硬编码,例如:在设置占位符和 SQL 语句的参数时,以及解析 ResultSet 时都需要通过硬编码来完成。
为了解决上述在 JDBC 编程中遇到的诸多问题,诞生了许多优秀的持久层框架,如:MyBatis,Hibernate,Spring Data JPA 等。它们的出现解决了 JDBC 编程中两个核心痛点:
- 封装 JDBC 访问数据库的过程,改善了访问数据库的复杂性;
- 实现了结果集到 Java 对象的映射,即实现了 ORM 功能。
Tips: 我的理解中,持久层框架的核心在于封装数据库访问的过程,而 ORM 的重心在于对象关系映射,只不过大部分框架中都实现了这两部分功能,因此你可能会听到一部分人将 MyBatis 这类框架称作持久层框架,另一部分人称之为 ORM 框架。
MyBatis 简介
这里引用 MyBatis 中文网 里给出的介绍:
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
接下来,我们用一张图来了解 MyBatis 发展过程中至关重要的 3 个时间节点:
MyBatis 的特点
与 JDBC 相比,MyBatis 封装了复杂的数据库访问过程,参数设定和结果集解析,使得访问数据库变得非常简单,相对的,代码量也会大幅度减少。
另外,MyBatis 支持自定义的 SQL 语句,允许开发者充分的利用数据库提供的特性,而另一款应用同样非常广泛的持久层框架 Hibernate,它将 Java 对象与数据库完全关联起来,使用 Hibernate 时操作的是 Java 对象,开发者无需要关心 SQL 语句的编写,除此之外,Hibernate 还提供了 HQL(Hibernate Query Language),语法与 SQL 类似,但操作的也是 Java 对象。由于 MyBatis 是直接操作 SQL 语句的,因此在性能上更加优秀(毕竟少了“中间商”),并且在实现复杂 SQL 语句和对 SQL 语句进行优化时会非常灵活和方便,例如:开发者很容易通过 MyBatis 实现多表联查,但在 Hibernate 中却较为困难。
直接操作 SQL 虽然带来了性能和灵活性上的有点,但也带来了一些问题。
虽然各家数据库厂商都支持 ANSI SQL 标准,但是也会提供一些不同的 SQL 方言和高级特性。因此,当你的应用程序中使用了 MyBatis,并且使用了数据库独有的高级特性,那么就表明你的应用程序与数据库是高度耦合的。
如果你经历过前两年各大国企,政府机构轰轰烈烈的“去 O 行动”,你可能会对此有比较强烈的感受。我有个朋友就经历过,他们的大部分应用都跑在 Oracle 上,而且使用了非常多 Oracle 的高级特性,他们在 Oracle 迁移到 MySQL 的过程中,修改了大量程序代码来实现 Oracle 的高级函数,另一点非常头疼的是,Oracle 在整体性能的表现上是优于 MySQL 的,为了保证应用程序的性能还需要对 SQL 语句进行优化。
最后一个特点是,MyBatis 对动态 SQL 的支持非常友好,可以通过几个简单标签实现 SQL 语句的动态查询条件。
Tips:
- Hibernate 也支持直接编写 SQL 语句,但这不是 Hibernate 的强项;
- 其它的特点,如简单易学,开发效率等特点,我们这里就不过多赘述了。
MyBatis 的应用场景
通过对 MyBatis 特点的了解,我们也能很容易的想到适合 MyBatis 的应用场景。
- 性能要求极高的应用:这类应用中,每一点都需要做到极致的性能优化,而 MyBatis 直接操作 SQL 语句,不需要通过 Java 对象翻译成 SQL 语句,并且能够非常灵活方便的进行 SQL 语句的优化,例如:大型电商项目;
- 业务逻辑复杂的应用:这类应用中,业务逻辑复杂,会涉及到比较多的复杂 SQL,MyBatis 直接操作 SQL 语句,在编写复杂 SQL 时会比较灵活简便,例如:金融行业的应用。
总而言之,如果需要对 SQL 语句进行性能优化,需要实现复杂的 SQL 语句,以及应用中需要使用到较多的动态 SQL 语句,MyBatis 都是非常不错的选择。
简单的例子
最后我们来写一个简单的例子,来感受下 MyBatis。
首先我们准备一个 Maven 项目,这里我的项目命名为 MyBatis-Tradition。Tradition 有“传统”的含义,这里我用来表示这个项目是仅使用了 MyBatis 而没有引入 Spring Boot。测试工程的完整结构如下:
依赖引入
在这个简单的例子中,我们所需要的软件及版本如下表:
软件 | 版本 | 说明 |
---|---|---|
Java | 17 | |
MyBatis | 3.5.15 | |
mysql-connector-j | 8.3.0 | |
log4j2 | 1.8.3 | 用于输出 SQL 日志 |
lombok | 1.18.30 | |
junit | 4.13.2 | 用于单元测试 |
完整的 POM.XML 文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wyz</groupId>
<artifactId>MyBatis-Tradition</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MyBatis-Tradition</name>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis.version>3.5.15</mybatis.version>
<mysql.version>8.3.0</mysql.version>
<log4j2.version>1.8.3</log4j2.version>
<junit.version>4.13.2</junit.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.basc.framework</groupId>
<artifactId>log4j2</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
创建 UserDo
依赖引入完成后,我们先来准备一个与数据库表(详见附录)对应的实体类 UserDo(User Data Object):
package com.wyz.entity;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class UserDo implements Serializable {
/**
* 用户Id
*/
private Integer userId;
/**
* 用户名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private String gender;
/**
* 证件类型
*/
private Integer idType;
/**
* 证件号
*/
private String idNumber;
}
Tips:实体类以及 Mapper.xml 文件可以通过相应的 MyBatis 生成工具来自动生成,我这里是通过 MyBatisX-Generator 生成后,使用了 lombok 注解替换了 Getter 方法和 Setter 方法。
创建 Mapper
Mapper 文件,即 MyBatis 中的映射器,用于映射 SQL 语句。接着我们来写一个 UserMapper.xml 文件,并定义查询全部用的 SQL 语句:
<?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.wyz.dao.UserDao">
<select id="selectAll" resultType="com.wyz.entity.UserDo" >
select user_id, name, age, gender, id_type, id_number from user
</select>
</mapper>
UserMapper.xml 在命名空间com.wyz.dao.UserDao
中定义了名为“selectAll”的查询语句,并将查询的结果映射到com.wyz.entity.UserDo
对象上。
现在版本的 MyBatis 中,命名空间的作用非常大,是必选项。命名空间有两个作用:
- 使用全限名隔离不同的 SQL 语句;
- 通过命名空间实现接口绑定。
因此,我们还需要给 UserMapper.xml 文件添加对应的 UserDao 接口。UserDao 接口的全部代码如下:
package com.wyz.dao;
public interface UserDao {
}
Tips:这里我并没有为 UserDao 接口添加 selectAll 方法,是因为在这个例子中我不会使用到对应的接口。
配置 MyBatis
做完上述工作后,我们再来配置 mybatis-config.xml 文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<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"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
mybatis-config.xml 文件中包含了 MyBatis 的核心配置,包括事务管理器(TransactionManager),数据源(DataSource)和映射器(Mapper)。
配置 log4j2
接下来我们配置 log4j2.xml 文件,只需要做少量的配置即可,完整的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">
<properties>
<!-- 文件输出格式 -->
<property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %c | %msg%n</property>
</properties>
<Appenders>
<!--控制台输出配置,只输出DEBUG级别以上的日志-->
<Console name="Console" target="SYSTEM_OUT">
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${PATTERN}"/>
</Console>
</Appenders>
<Loggers>
<Root level="DEBUG">
<Appender-Ref ref="Console"/>
</Root>
<!-- SQL语句配置 -->
<logger name="org.apache.ibatis" level="DEBUG"/>
</Loggers>
</Configuration>
测试 UserMapper
完成了以上工作后,我们为其编写一个测试类 UserMapperTest,源码如下:
package com.wyz.mapper;
import com.wyz.entity.UserDo;
import lombok.extern.slf4j.Slf4j;
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 org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
@Slf4j
public class UserMapperTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init() throws IOException {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
}
@Test
public void testSelectAll() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<UserDo> users = sqlSession.selectList("selectAll");
for(UserDo userDo:users) {
log.info(userDo.getName());
}
sqlSession.close();
}
}
MyBatis 应用是以 SqlSessionFactory 为核心的,在这个测试案例中,我们通过 MyBatis 的配置文件 mybatis-config.xml 创建了 SqlSessionFactory,接着使用 SqlSessionFactory 开启了 SqlSession,并进行数据库查询。
Tips:附录中提供了完整的不使用 XML 构建 SqlSessionFactory 的例子。
附录 1:SQL 语句
创建数据库 mybatis:
create schema mybatis collate utf8mb4_general_ci;
创建表 user:
create table user (
user_id int not null comment '用户Id' primary key,
name varchar(50) not null comment '用户名',
age int not null comment '年龄',
gender varchar(50) not null comment '性别',
id_type int not null comment '证件类型',
id_number varchar(50) not null comment '证件号',
constraint idx_id_number unique (id_number)
);
初始化数据:
INSERT INTO mybatis.user (user_id, name, age, gender, id_type, id_number) VALUES (1, '小明', 18, 'M', 1, '110202402217865');
INSERT INTO mybatis.user (user_id, name, age, gender, id_type, id_number) VALUES (2, '小红', 18, 'F', 1, '110202402217866');
附录 2:不使用 XML 构建 SqlSessionFactory
除了使用 mybatis-config 构建 SqlSessionFactory 外,还可以通过 Java 的方式构建 SqlSessionFactory,MyBatis 也提供了所有与 XML 文件等价的配置项。由于官网的例子并不完整,导致很多小伙伴测试失败,这里我给出一个比较完整的例子供大家参考。
首先,我们删除 mybatis-config.xml 文件和 UserMapperTest,保证 SqlSessionFactory 并不是通过 XML 文件创建的。
接着,我们创建测试类 UserMapperWithoutXMLTest,完整的代码如下:
package com.wyz.mapper;
import com.wyz.dao.UserDao;
import com.wyz.entity.UserDo;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.sql.DataSource;
import java.util.List;
@Slf4j
public class UserMapperWithoutXMLTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init() {
DataSource dataSource = new PooledDataSource("com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/mybatis", "root", "123456");
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(UserDao.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
}
@Test
public void testSelectAll() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<UserDo> users = sqlSession.selectList("selectAll");
for(UserDo userDo:users) {
System.out.println(userDo.getName());
}
sqlSession.close();
}
}
我们重点关注 init 方法,可以看到通过 Java 去构建 SqlSessionFactory 的顺序与 mybatis-config.xml 中的标签顺序是一致的,而且内容也是完全相同的,稍有差异的是,在 mybatis-config.xml 文件中,映射器添加的是 UserMapper.xml 文件,而在通过 Java 构建 SqlSessionFactory 的过程中,映射器添加的是 UseDao。
因为之前的 UserDao 中并没有定义 UserMapper.xml 中对应的接口,所以这里我们要在 UserDao 中加上这个接口:
public interface UserDao {
List<UserDo> selectAll();
}
很多小伙伴以为到这里就大功告成了,但实际上这里才是踩坑的开始。当你满怀信心的点击测试时,迎接你的应该是下面的“红色”。
明明在 UserDao 中定义了 selectAll 接口,并且也有对应的 UserMapper.xml 文件,为什么会出现“Mapped Statements collection does not contain value for selectAll”的错误?
如果选择通过注解的方式定义 SQL 语句,这里是没有问题的,例如:
public interface UserDao {
@Select("select user_id, name, age, gender, id_type, id_number from user")
List<UserDo> selectAll();
}
但是注解的方式在处理复杂 SQL 时多少会有些力不从心,我们还是希望通过 XML 的方式完成 SQL 的映射,那么该如何解决呢?
如果你了解 MyBatis 源码的话,你会知道 MyBatis 在初始化时会自动加载通过Configuration#addMapper
添加的映射器的包下的同名 XML 文件,在这个例子中,MyBatis 会尝试加载 com.wyz.dao 包下的 UserDao.xml 文件,那么我们将 UserMapper.xml 修改成 UserDao.xml 后,并移动到 com.wyz.dao 下是不是就可以了呢?
答案是不行,因为编译是并不会主动引入 src/mian/java 路径下的 XML 文件,还需要修改 pom.xml 文件,如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wyz</groupId>
<artifactId>MyBatis-Tradition</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MyBatis-Tradition</name>
<!-- 省略 -->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>
注意,这里需要重新配置引入 src/main/resources 路径下的文件,否则 log4j2.xml 文件不会生效。
至此,我们就完成了不使用 XML 构建 SqlSessionFactory,不过通常项目中不会选择这种方式进行配置,而是选择更加直观和易于管理的 mybatis-config.xml 的方式进行配置。
Tips:
- 关于 MyBatis 自动加载 XML 文件的源码我们会在后续的文章中再做具体分析,这里就不过多的解释了;
- 在与 Spring Boot 的整合中,利用 Spring Boot 的自动配置机制,就不需要通过 mybatis-config.xml 进行配置了。
好了,今天的内容就到这里了,如果本文对你有帮助的话,希望多多点赞支持,如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核 Java 技术的金融摸鱼侠王有志,我们下次再见!