MyBatis入门

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

大家好,我是王有志。今天开始我会和大家一起来学习在 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 数据库访问,并执行一条简单的查询语句:

java 复制代码
// 数据库地址
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 的特点

与 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。测试工程的完整结构如下:

依赖引入

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

软件 版本 说明
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 文件如下:

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):

java 复制代码
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 复制代码
<?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 接口的全部代码如下:

java 复制代码
package com.wyz.dao;

public interface UserDao {
}

Tips:这里我并没有为 UserDao 接口添加 selectAll 方法,是因为在这个例子中我不会使用到对应的接口。

配置 MyBatis

做完上述工作后,我们再来配置 mybatis-config.xml 文件:

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 复制代码
<?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,源码如下:

java 复制代码
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:

sql 复制代码
create schema mybatis collate utf8mb4_general_ci;

创建表 user:

sql 复制代码
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)
);

初始化数据:

sql 复制代码
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,完整的代码如下:

java 复制代码
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 中加上这个接口:

java 复制代码
public interface UserDao {
  List<UserDo> selectAll();
}

很多小伙伴以为到这里就大功告成了,但实际上这里才是踩坑的开始。当你满怀信心的点击测试时,迎接你的应该是下面的"红色"。

明明在 UserDao 中定义了 selectAll 接口,并且也有对应的 UserMapper.xml 文件,为什么会出现"Mapped Statements collection does not contain value for selectAll"的错误?

如果选择通过注解的方式定义 SQL 语句,这里是没有问题的,例如:

java 复制代码
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 文件,如下:

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 技术的金融摸鱼侠 王有志,我们下次再见!

相关推荐
测试界萧萧31 分钟前
15:00开始面试,15:06就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
Warren981 小时前
Java面试八股Spring篇(4500字)
java·开发语言·spring boot·后端·spring·面试
背帆1 小时前
go的interface接口底层实现
开发语言·后端·golang
IT成长史2 小时前
deepseek梳理java高级开发工程师springboot面试题2
java·spring boot·后端
是麟渊2 小时前
【大模型面试每日一题】Day 17:解释MoE(Mixture of Experts)架构如何实现模型稀疏性,并分析其训练难点
人工智能·自然语言处理·面试·职场和发展·架构
qq_266348733 小时前
springboot AOP中,通过解析SpEL 表达式动态获取参数值
java·spring boot·后端
bing_1583 小时前
MQTT 在Spring Boot 中的使用
java·spring boot·后端·mqtt
阑梦清川6 小时前
关于Go语言的开发环境的搭建
开发语言·后端·golang
lyrhhhhhhhh6 小时前
Spring 模拟转账开发实战
java·后端·spring
tonngw6 小时前
【Mac 从 0 到 1 保姆级配置教程 12】- 安装配置万能的编辑器 VSCode 以及常用插件
git·vscode·后端·macos·开源·编辑器·github