likes
comments
collection
share

MyBatis入门

作者站长头像
站长
· 阅读数 4

大家好,我是王有志。今天开始我会和大家一起来学习在 Java 应用程序中使用非常广泛的持久层框架 MyBatis。作为 MyBatis 系列的第一篇文章,我会先对 MyBatis 以及诞生的意义做一个简单的介绍,最后我们在一起动手完成一个简单的例子。Tips:文章最后的部分,我会对 MyBatis 中文网上“不使用 XML 构建 SqlSessionFactory”的例子进行补充。MyBatis入门

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 的几个步骤:

  1. 加载数据库驱动;
  2. 获取数据库链接;
  3. 定义 SQL 语句;
  4. 获取 PreparedStatement,并设置 SQL 参数;
  5. 通过 PreparedStatement 执行 SQL 语句,并获取结果集;
  6. 分析并处理结果集;
  7. 释放资源(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入门

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 的应用场景。

  1. 性能要求极高的应用:这类应用中,每一点都需要做到极致的性能优化,而 MyBatis 直接操作 SQL 语句,不需要通过 Java 对象翻译成 SQL 语句,并且能够非常灵活方便的进行 SQL 语句的优化,例如:大型电商项目;
  2. 业务逻辑复杂的应用:这类应用中,业务逻辑复杂,会涉及到比较多的复杂 SQL,MyBatis 直接操作 SQL 语句,在编写复杂 SQL 时会比较灵活简便,例如:金融行业的应用。

总而言之,如果需要对 SQL 语句进行性能优化,需要实现复杂的 SQL 语句,以及应用中需要使用到较多的动态 SQL 语句,MyBatis 都是非常不错的选择

简单的例子

最后我们来写一个简单的例子,来感受下 MyBatis。首先我们准备一个 Maven 项目,这里我的项目命名为 MyBatis-Tradition。Tradition 有“传统”的含义,这里我用来表示这个项目是仅使用了 MyBatis 而没有引入 Spring Boot。测试工程的完整结构如下:MyBatis入门

依赖引入

在这个简单的例子中,我们所需要的软件及版本如下表:

软件版本说明
Java17
MyBatis3.5.15
mysql-connector-j8.3.0
log4j21.8.3用于输出 SQL 日志
lombok1.18.30
junit4.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());
    }
  }
}

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());
    }
  }
}

我们重点关注 init 方法,可以看到通过 Java 去构建 SqlSessionFactory 的顺序与 mybatis-config.xml 中的标签顺序是一致的,而且内容也是完全相同的,稍有差异的是,在 mybatis-config.xml 文件中,映射器添加的是 UserMapper.xml 文件,而在通过 Java 构建 SqlSessionFactory 的过程中,映射器添加的是 UseDao。因为之前的 UserDao 中并没有定义 UserMapper.xml 中对应的接口,所以这里我们要在 UserDao 中加上这个接口:

public interface UserDao {
  List<UserDo> selectAll();
}

很多小伙伴以为到这里就大功告成了,但实际上这里才是踩坑的开始。当你满怀信心的点击测试时,迎接你的应该是下面的“红色”。MyBatis入门明明在 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 技术的金融摸鱼侠王有志,我们下次再见!