Spring Boot 整合 MyBatis

John同學發表於2022-05-13

MyBatis 簡介

MyBatis 是一款優秀的持久層框架,它支援自定義 SQL、儲存過程以及高階對映。MyBatis 免除了幾乎所有的 JDBC 程式碼以及設定引數和獲取結果集的工作。MyBatis 可以通過簡單的 XML 或註解來配置和對映原始型別、介面和 Java POJO(Plain Ordinary Java Objects,簡單 Java 物件)為資料庫中的記錄。

參考自 MyBatis 的官方簡介。

MyBatis 作為一款優秀的持久層框架,具有如下優點:

  1. 小巧並且簡單易學。

  2. 相比於 JDBC 減少了大量冗餘的程式碼。

  3. 將 SQL 語句與程式程式碼進行分離,降低了耦合,便於管理。

  4. 提供 XML 標籤,支援編寫動態 SQL 語句。

  5. 提供對映標籤,支援 Java 物件的屬性與資料表欄位的對映關係。

MyBatis 實踐

下面我們建立一個 Spring Boot 專案,整合 MyBatis,實現簡單的 CRUD 功能。

1. 引入依賴

POM 檔案如下:

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-mybatis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-mybatis</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2. 配置 MySQL 和 MyBatis

配置檔案 application.yml 的內容如下:

# 配置 MySQL
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

# 配置 MyBatis
mybatis:
  mapper-locations: classpath:mapper/*
  type-aliases-package: com.example.entity
  configuration:
    map-underscore-to-camel-case: true

MyBatis 的配置項中:

  • mapper-locations:用來指定 mapper.xml 檔案的路徑,該檔案用於編寫 SQL 語句。

  • type-aliases-package:用來設定別名,它的作用是告訴 MyBatis 需要設定別名的實體類的所在的包。預設情況下,MyBatis 會使用實體類的非限定類名來作為它的別名,如將 com.example.entity.User 的別名設定為 Useruser(別名不區分大小寫)。當然,MyBatis 也支援自定義別名,這個我們在後文中再聊。

  • map-underscore-to-camel-case:用來開啟駝峰命名自動對映,如將資料表中的欄位 user_name 對映到實體物件的屬性 userName。

3. 實體類

編寫簡單的 User 類:

package com.example.entity;

import lombok.Data;

import java.util.Date;

/**
 * @Author john
 * @Date 2021/11/14
 */
@Data
public class User {

    private long id;

    private String userName;

    private int age;

    private String address;

    private Date createTime;

    private Date updateTime;
}

User 類中封裝了使用者的 id、姓名、年齡、地址、建立時間以及修改時間等資訊。

4. 建立 user 表

user 表的欄位設計如下:

5. 編寫 Mapper 介面和 mapper 檔案

首先編寫 UserMapper 介面:

package com.example.mapper;

import com.example.entity.User;

/**
 * @Author john
 * @Date 2021/11/16
 */
public interface UserMapper {
    
    void insertUser(User user);
    
    User findUserById(long id);
}

介面中定義了兩個方法,insertUser 用來向資料表中插入一條記錄,findUserById 用來通過 id 查詢 User。

上述操作完成後,我們在 resources 資料夾中建立 mapper/user-mapper.xml 檔案(檔案路徑在配置檔案 application.yml 中設定)。user-mapper.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.example.mapper.UserMapper">

    <sql id="insertFields">
        user_name, age, address, gmt_create, gmt_modified
    </sql>

    <sql id="selectFields">
        id, user_name, age, address, gmt_create, gmt_modified
    </sql>

    <resultMap id="UserMap" type="User">
        <result column="id" jdbcType="INTEGER" property="id"/>
        <result column="user_name" jdbcType="VARCHAR" property="userName"/>
        <result column="age" jdbcType="INTEGER" property="age"/>
        <result column="address" jdbcType="VARCHAR" property="address"/>
        <result column="gmt_create" jdbcType="DATE" property="createTime" />
        <result column="gmt_modified" jdbcType="DATE" property="updateTime" />
    </resultMap>

    <select id="findUserById" parameterType="Long" resultMap="UserMap">
        select
        <include refid="selectFields"/>
        from user
        where id = #{id}
    </select>

    <insert id="insertUser" parameterType="User" keyProperty="id">
        insert into user (<include refid="insertFields"/>)
        values(#{userName}, #{age}, #{address}, UTC_TIMESTAMP(), UTC_TIMESTAMP())
    </insert>

</mapper>

可以看到,Mapper 介面中定義的是 CRUD 相關的方法,mapper.xml 檔案中定義的是具體的 SQL 語句。MyBatis 允許我們將 Mapper 介面與 mapper.xml 檔案關聯在一起,這樣當呼叫 Mapper 介面中的方法時,實際的處理邏輯為執行 mapper.xml 檔案中對應的 SQL 語句。關聯 Mapper 介面和 mapper.xml 檔案時需要保證:

  • Mapper 介面的全限定名對應 mapper.xml 檔案的 namespace 值。

  • Mapper 介面的方法名對應 statement(每一個 SQL 就是一個 statement)的 id 值。

  • Mapper 介面中方法接收的引數對應 statement 的入參。

  • Mapper 介面中方法的返回值對應 statement 的出參。

下面介紹一下 mapper.xml 檔案中幾個重要標籤的含義:

  • <sql> 標籤:用於定義複用的 SQL 片段,如果多個 SQL 需要操作相同的欄位集,那麼就可以使用 <sql> 標籤將這些欄位提取出來,然後在 SQL 語句中直接引用即可。引用的語法為 <include refid=" "/>,其中 refid 的值就是 <sql> 的 id 值。

  • <resultMap> 標籤:用於建立資料表欄位與實體屬性的對映關係,在查詢操作中,MyBatis 會根據查詢到的欄位名找到 POJO 對應的屬性名,然後呼叫該屬性的 setter 方法進行賦值。如果資料表的欄位名與實體類的屬性名完全相同,或者符合駝峰式命名對映的規則,那麼 MyBatis 可以直接完成賦值操作。否則的話,就需要我們使用 <resultMap> 標籤建立自定義的對映規則,告訴 MyBatis 欄位和屬性之間應該如何對映。本實驗中,user 表的 id 會自動對映為 User 物件的 id,user 表的 user_name 也會自動對映為 User 物件的 userName。但是 gmt_create 和 gmt_modified 不會對映為 createTime 和 updateTime,因為欄位名和屬性名既不完全一致,也不符合駝峰式命名對映的規則,所以這裡我們需要使用 <resultMap> 來建立新的對映關係,其中屬性 id 用於指明該 resultMap 的標誌,屬性 type 用於指明對映的實體類。

  • <select> 標籤:用於執行查詢操作。

  • <insert> 標籤:用於執行插入操作。

實際上,MyBatis 賦值時不一定會呼叫實體類屬性的 setter 方法,因為我們在編碼時可能並沒有新增該方法。以 User 類的屬性 id 為例,如果我們新增了 setId 方法,那麼 MyBatis 會通過反射獲取到 setId 對應的 MethodInvoker,然後呼叫 setId 方法為 id 賦值;如果未設定 setId 方法,那麼 MyBatis 會獲取屬性 id 對應的 SetFieldInvoker,然後為屬性賦值。詳見 MetaObject 類的 setValue 方法。

接下來介紹 SQL 語句中幾個重要屬性的含義:

  • parameterType:用於指定 SQL 語句的入參型別(可以是基本資料型別或者 JavaBean),該型別需要與對應的介面方法的入參型別一致。如果我們設定了別名,那麼也可以使用別名作為引數,例如使用 Useruser 代替 com.example.entity.User

  • resultMap:用於指定 SQL 語句的出參型別,以 insertUser 方法為例,在 Mapper 介面中,該方法的返回值為 User 型別,所以對應的 SQL 語句的返回值也應為 User 型別,由於 User 物件需要使用 <resultMap> 進行屬性對映,所以我們將自定義的 UserMap 來作為 SQL 語句的返回值型別。

  • keyProperty:用於指定主鍵在 POJO 中對應的屬性名,需要配合資料庫的自增主鍵來使用。以 user 表為例,我們在建表的時候將表的主鍵 id 設定為了資料庫自增 id,因此在將 User 物件持久化到資料庫之前不需要為屬性 id 設定初始值,MySQL 會自動幫我們賦值,keyProperty 的作用就是告訴 MyBatis 哪個屬性是主鍵。

除了 resultMap 外,resultType 屬性也可用於指定出參型別。如果我們將 user 表中的欄位 gmt_create 和 gmt_modified 分別改為 create_time 和 update_time,那麼就不需要使用 <resultMap> 標籤來配置對映規則,因為 user 表的所有欄位都可以和 User 物件的屬性一一對應,這樣在 SQL 語句中,就可以將 resultMap="UserMap" 替換為 resulType="User"resulType="user"。另外,在本實驗中,resultMap 標籤也可以定義為:

<resultMap id="UserMap" type="User">
    <result column="gmt_create" jdbcType="DATE" property="createTime" />
    <result column="gmt_modified" jdbcType="DATE" property="updateTime" />
</resultMap>

因為其他欄位會自動對映,不需要額外書寫。

6. 編寫 Service

建立 UserService:

package com.example.service;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author john
 * @Date 2021/11/16
 */
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public void insertUser(User user) {
        userMapper.insertUser(user);
    }

    public User findUserById(long id) {
        return userMapper.findUserById(id);
    }
}

在 UserService 中注入 UserMapper 物件,並呼叫相關方法來新增/查詢 User。

為了能夠正常注入 UserMapper 物件,我們還需要再啟動類上新增 @MapperScan 註解,並指定 Mapper 介面所在的包:

package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.mapper")
public class SpringbootMybatisApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootMybatisApplication.class, args);
    }

}

com.example.mapper 包下的所有 Mapper 介面都會被 Spring 掃描。

除了在啟動類上新增 @MapperScan 註解外,還可以在 Mapper 介面上直接新增 @Mapper 註解,這種方法相對比較麻煩,因為實際中我們可能會有多個 Mapper 介面,這樣就需要新增多個註解。

7. 測試

編寫測試介面:

package com.example;

import com.example.entity.User;
import com.example.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootMybatisApplicationTests {

    @Autowired
    private UserService service;

    @Test
    public void addUser(){
        User user = new User();
        user.setUserName("John");
        user.setAge(24);
        user.setAddress("BUPT");
        service.insertUser(user);
    }

    @Test
    public void findUser(){
        System.out.println(service.findUserById(1));
    }
}

首先執行 addUser() 方法,執行成功後查詢資料表,得到如下資訊:

然後執行 findUser() 方法,執行結果如下:

至此,SpringBoot 整合 MyBatis 測試成功!

MyBatis 設定別名的方式

方式一:在配置檔案 application.yml 中新增配置。

yml 檔案的配置內容如下:

mybatis:
  type-aliases-package: com.example.entity

本實驗採用此種方式設定別名,預設情況下實體類的別名為其類名,嚴格來說是首字母小寫的非限定類名,由於別名不區分大小寫,所以 UseruseruSer 的效果都是相同的。

MyBatis 也支援自定義別名,我們只需要在實體類上新增 @Alias 註解,就可以為其設定別名:

package com.example.entity;

import lombok.Data;
import org.apache.ibatis.type.Alias;

import java.util.Date;

/**
 * @Author john
 * @Date 2021/11/14
 */
@Data
@Alias("hello")
public class User {

    private long id;

    private String userName;

    private int age;

    private String address;

    private Date createTime;

    private Date updateTime;
}

上述程式碼中,我們將 User 類的別名設定為了 hello。注意,若要使 @Alias 註解生效,必須配置 type-aliases-package 來指定實體類的包路徑。另外,@Alias 會使預設的別名變得無效,例如在本實驗中,User 類的別名只能是 hello,而不能是 Useruser 等。

方式二:使用 MyBatis 的配置檔案 filename.xml。

首先在 yml 檔案中設定 MyBatis 配置檔案 filename.xml(filename 是配置檔案的名稱)的路徑:

# 配置MyBatis
mybatis:
  mapper-locations: classpath:mapper/*
  config-location: classpath:mybatis/mybatis-config.xml #MyBatis配置檔案

然後在 resource 資料夾下建立 MyBatis 的配置檔案 mapper/mybatis-config.xml(路徑和檔名在 config-location 中設定),配置檔案內容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <package name="com.example.entity"/>
    </typeAliases>

</configuration>

幾個重要標籤的含義為:

  • <setting> 標籤:用於開啟駝峰命名對映,其效果與在 yml 檔案中配置 map-underscore-to-camel-case: true 是相同的。

  • <typeAliases> 標籤:用於配置別名,子標籤 <package> 可以讓 MyBatis 掃描指定包下的實體類,其效果與在 yml 檔案中配置 type-aliases-package: com.example.entity 是相同的。

在方式一中,我們可以使用 @Alias 註解自定義別名,而在方式二中,我們可以通過<typeAliases> 的子標籤 <typeAlias> 來設定別名:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <typeAlias type="com.example.entity.User" alias="hello"/>
    </typeAliases>

</configuration>

<typeAlias> 標籤不需要配置 type-aliases-package 就可以生效,且該標籤與 <package> 標籤並不衝突,也就是說如果我們新增了 <package name="com.example.entity"/>,那麼 User 類的別名既可以是 hello,也可以是 Useruser 等。當然,方式二中也可以新增 @Alias 註解,但新增了該註解後,User 類的別名只能為 hello 或 @Alias 註解指定的別名。

相關文章