SpringBoot 整合 MyBatis,實現 CRUD 示例

前言

有 Java Web 應用開發經驗的同學應該很熟悉 Controller/Service/Dao 這樣的三層結構設計,MyBatis 就是實現 Dao 層的主流方式之一,用於完成數據庫的讀寫操作;Dao 層服務於 Service 層,用於完成完成業務邏輯操作。

本文聚焦於 SpringBoot 和 MyBatis 的整合使用,考慮到引入 Controller 和 Service 層描述起來會比較複雜,因此僅涉及 Dao 層。

創建項目/模塊

在 Maven 項目 SpringBoot 中添加模塊 mybatis 用於演示,mybatis pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
         xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>springboot</artifactId>
    <groupId>tech.exchange</groupId>
    <version>0.1</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>

  <artifactId>mybatis</artifactId>
</project>

按常規套路,都是 Controller 調用 Service,Service 調用 Dao;如前所述,不引入 Controller 和 Service,那麼怎麼實現 Dao 的調用呢?

其實,SpringBoot 不僅可以是一個 Web 應用,也可以是一個 命令行(Console) 應用,類似於一個 Java Main 的應用程序。

SpringBoot Console Application

演示如何創建一個 SpringBoot 命令行應用。

  1. 添加依賴
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

注意:Web 應用需要添加依賴 spring-boot-starter-web,命令行 應用需要添加依賴 spring-boot-starter,兩者是不一樣的。

  1. 創建 Main
package tech.exchange.springboot.mybatis;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author yurun
 */
@SpringBootApplication
public class Main implements CommandLineRunner {

  @Override
  public void run(String... args) throws Exception {

  }

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

這裡的 Main,本質就是一個 Java Main,只不過額外添加註解和實現特定接口方法。

CommandLineRunner

CommandLineRunner is a simple Spring Boot interface with a run method. Spring Boot will automatically call the run method of all beans implementing this interface after the application context has been loaded.

CommandLineRunner 是 SpringBoot 的一個接口,它只有一個 run 方法;SpringBoot 容器加載完成之後,所有實現 CommandLineRunner 接口的 Beans 都會被自動調用 run 方法。

Main 就相當於實現接口 CommandLineRunner 的一個特殊 Bean,SpringBoot 容器加載完成之後,run 方法會被自動執行。我們可以在 run 方法內部實現 Dao 的調用。

SpringBoot 集成 MyBatis

MyBatis 官方提供了 SpringBoot 的集成方案,過程很簡單,添加依賴 mybatis-spring-boot-starter 即可:

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

SpringBoot 和 MyBatis 集成完成。

本文數據庫使用 MySQL,添加驅動 mysql-connector-java :

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

創建數據庫/表

創建演示使用的數據表 mytable:

CREATE TABLE `mytable` (
  `id` int NOT NULL AUTO_INCREMENT,
  `col1` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL,
  `col2` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

其中,主鍵 id 使用自增(AUTO_INCREMENT)策略。

配置數據源/連接池

SpringBoot 和 數據庫 的交互需要通過數據源(DataSource)實現,數據源僅需要使用配置文件(application.yml)聲明相關屬性即可,不需要額外操作。MyBatis 也會使用數據源完成自身的初始化。

數據源

創建 application.yml(src/main/resources/application.yml):

spring:
  datasource:
    url: jdbc:mysql://mysql_dev:13306/yurun?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
  • url:MySQL 連接信息,包括地址、端口、數據庫名稱和其他參數;其中,建議設置 useUnicode=true 和 characterEncoding=UTF-8,避免出現中文亂碼的情況;
  • username:MySQL 用戶名;
  • password:MySQL 密碼;

連接池

連接池就是緩存若干數據庫(MySQL)的連接(Connection),避免連接的重複創建銷毀,減少 SQL 執行耗時。

SpringBoot 集成 MyBatis 時,會自動集成連接池 HikariCP,這也是 SpringBoot 官方推薦使用的,連接池這裡使用默認配置。

CRUD

MyBatis 對於數據庫的讀寫操作是通過 Mapper 實現的,Mapper 有兩種形式:

  • 接口(Interface) + XML
  • 接口(Interface) + 註解

這兩種形式並沒有絕對意義上的好壞之分,使用 接口 + 註解,只需要編寫一個接口,實現方式會更簡潔一些,但靈活性會相對弱一些;使用 接口 + XML,除編寫接口之外,還需要編寫額外的 XML 文件,但靈活性更強。

本文使用 接口 + XML 的形式,以讀寫數據表 mytable 為例,創建接口和XML文件。

創建接口

接口文件:src/main/java/tech/exchange/springboot/mybatis/dao/MyTableMapper.java

package tech.exchange.springboot.mybatis.dao;

import org.apache.ibatis.annotations.Mapper;

/**
 * @author yurun
 */
@Mapper
public interface MyTableMapper {
}

創建 XML

XML文件:src/main/resources/mapper/MyTableMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.exchange.springboot.mybatis.dao.MyTableMapper">
</mapper>

接口文件名稱建議和XML文件名稱保持一致(非強制)方便查找。

XML文件位於目錄 src/main/resources/mapper,需要配置 application.yml,使得 Mapper 可以被 SpringBoot 正確掃描加載。

mybatis:
  mapper-locations:
    - classpath:mapper/*.xml

相當於告訴 SpringBoot,到類路徑下的 mapper 目錄下面掃描加載 Mapper。

XML namespace 用於綁定 Mapper 的接口和XML文件,兩者必須一一對應,namespace 的值必須為接口的全類名。

創建實體類

package tech.exchange.springboot.mybatis.model;

/**
 * @author yurun
 */
public class MyRow {
  private int id;
  private String col1;
  private String col2;

  ......
}

實體類 MyRow 的字段與數據庫表 MyTable 的字段一一對應(非強制),MyRow 的一個實例表示 MyTable 的一行記錄。

Create

  1. 插入單行記錄

    XML插入元素

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "//mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="tech.exchange.springboot.mybatis.dao.MyTableMapper">
      <insert id="insertMyRow">
        INSERT INTO
          mytable (col1, col2)
        VALUES
          (#{col1}, #{col2})
      </insert>
    </mapper>
    

    MyBatis XML 可以使用 #{name} 的形式聲明參數;其中,name 為參數名稱。

    那麼,這些參數來源於哪裡?元素 insert 有一個 屬性 parameterType 用於指定參數類型,如:

      <insert id="insertMyRow" parameterType="tech.exchange.springboot.mybatis.model.MyRow">
        ......
      </insert>
    

    表示會使用 MyRow 的實例來傳遞元素 insert 所需的參數,#{col1} 和 #{col2} 分別對應着 MyRow (實例)的 字段 col1 和 col2。

    大多數情況下,我們可以省略屬性 parameterType,MyBatis 會為我們自動推斷這個類型。

    接口方法

    package tech.exchange.springboot.mybatis.dao;
    
    import org.apache.ibatis.annotations.Mapper;
    import tech.exchange.springboot.mybatis.model.MyRow;
    
    import java.util.List;
    
    /**
     * @author yurun
     */
    @Mapper
    public interface MyTableMapper {
      /**
       * 插入一行記錄。
       *
       * @param row 一行記錄
       * @return 影響行數
       */
      int insertMyRow(MyRow row);
    }
    
    

    接口方法名稱需要與XML插入元素id保持一致,調用接口方法時會執行對應名稱XML元素中的SQL語句。

    接口方法使用 MyRow 實例傳入參數,XML插入元素 INSERT 語句中的參數 #{col1}、#{col2} 需要與實體類 MyRow 中的字段 col1、col2 名稱保持一致(字段順序可任意);也就是說,接口方法執行時,會將 MyRow 實例對象 row 中的字段值按名稱賦值給 INSERT 語句中的各個參數。

    接口方法的返回值為影響行數,插入單選記錄返回值為1。

    :插入、修改和刪除的接口方法返回值均為影響行數。

    插入單行記錄

    在 Main 中添加 MyTableMapper 實例 mapper:

      @Autowired
      private MyTableMapper mapper;
    

    @Autowired 表示 SpringBoot 會根據接口類型自動注入相應的實例。

    在 Main run 方法中添加插入記錄的代碼:

        MyRow row = new MyRow();
    
        row.setCol1("a");
        row.setCol2("b");
    
        int value = mapper.insertMyRow(row);
        System.out.println(value);
    

    因為數據表 mytable 的主鍵 id 是自增的,所以不需要設置 MyRow id 的值。

    Main 完整代碼:

    package tech.exchange.springboot.mybatis;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import tech.exchange.springboot.mybatis.dao.MyTableMapper;
    import tech.exchange.springboot.mybatis.model.MyRow;
    
    /**
     * @author yurun
     */
    @SpringBootApplication
    public class Main implements CommandLineRunner {
    
      @Autowired
      private MyTableMapper mapper;
    
      @Override
      public void run(String... args) throws Exception {
        MyRow row = new MyRow();
    
        row.setCol1("a");
        row.setCol2("b");
    
        int value = mapper.insertMyRow(row);
        System.out.println(value);
      }
    
      public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
      }
    }
    
  2. 插入單行記錄,且獲取已插入記錄的主鍵ID

    如前文所述,數據表 mytable 主鍵字段 id 是支持自增的,這裡所說的就是獲取已插入記錄 id 的自增值。

    XML插入元素

      <insert id="insertMyRow" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO
        mytable (col1, col2)
        VALUES
        (#{col1}, #{col2})
      </insert>
    
    • useGeneratedKeys 值為 true,表示通知 MyBatis 使用 JDBC getGeneratedKeys 方法獲取已插入記錄自增主鍵的值;
    • keyProperty 值為 id,表示 MyBatis 會將獲取到的自增主鍵的值,賦值給 MyRow 實例的字段 id;

    插入單行記錄,且獲取已插入記錄的主鍵ID

        // 創建記錄
        MyRow row = new MyRow();
    
        row.setCol1("a");
        row.setCol2("b");
    
        // 插入
        int value = mapper.insertMyRow(row);
        System.out.println(value);
        
        // 獲取主鍵ID
        System.out.println(row.getId());
    

    插入方法執行完成之後

        mapper.insertMyRow(row)
    

    即可以獲取主鍵ID

        row.getId()
    

    獲取主鍵 ID 是通過 row 記錄實例獲取的。

  3. 插入多行記錄

    插入多行記錄和插入單行記錄,大體類似,關鍵在於 INSERT 語句如何表述插入多行操作。

    XML插入元素

      <insert id="insertMyRows" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO
        mytable (col1, col2)
        VALUES
        <foreach item="item" collection="list" separator=",">
          (#{item.col1}, #{item.col2})
        </foreach>
      </insert>
    

    插入多行記錄的關鍵就是需要使用 foreach 元素,循環生成插入多行記錄所需的 SQL 語句:

    • collection=”list”,表示接口方法會通過集合(List)傳入多行記錄,如:List
    • item=”item”,表示處理某一行記錄時,該行記錄的名稱(別名)為 item,可以通過 item 獲取記錄字段的值;如:item.col1,表示獲取某個記錄(row)字段 col1 的值;
    • separator=”,”,表示多行記錄的處理結果以逗號進行連接;如:(a1, b1), (a2, b2)。

    還可以有另一種寫法:

      <insert id="insertMyRows" useGeneratedKeys="true" keyProperty="id">
        <foreach item="item" collection="list" separator=";">
          INSERT INTO
          mytable (col1, col2)
          VALUES
          (#{item.col1}, #{item.col2})
        </foreach>
      </insert>
    

    同樣是插入多行記錄,第一種寫法是通過一次請求(MySQL)執行一條 SQL 語句實現的,第二種寫法是通過一次請求執行多條 SQL 語句實現的,這裡僅僅是為了演示 foreach 的靈活用法,實際批量場景中不推薦這樣使用。

    注意:MySQL 一次請求執行多條 SQL 語句需要數據庫連接Url添加 allowMultiQueries=true。

    接口方法

      /**
       * 插入多行記錄。
       *
       * @param rows 多行記錄
       * @return 影響行數
       */
      int insertMyRows(List<MyRow> rows);
    

    相較於插入單行記錄,接口方法的參數為 MyRow(單數);插入多行記錄,接口方法的參數為 List(複數)。

    插入多行記錄

        MyRow row1 = new MyRow();
    
        row1.setCol1("a1");
        row1.setCol2("b1");
    
        MyRow row2 = new MyRow();
    
        row2.setCol1("a2");
        row2.setCol2("b2");
    
        // 多行記錄
        List<MyRow> rows = new ArrayList<>();
    
        rows.add(row1);
        rows.add(row2);
    
        // 插入,返回影響行數
        int value = mapper.insertMyRows(rows);
        System.out.println(value);
    
        rows.forEach(row -> {
          // 獲取已插入記錄的主鍵ID
          System.out.println(row.getId());
        });
    

    和插入單行記錄類似,多行記錄插入完成之後,每一條記錄的 id 字段也會被 MyBatis 自動賦予相應的主鍵ID值。

Read

  1. 查詢單行記錄

    指定記錄ID,查詢相應的一行記錄。

    XML查詢元素

      <select id="selectMyRow" resultType="tech.exchange.springboot.mybatis.model.MyRow">
        SELECT
          *
        FROM
          mytable
        WHERE
          id = #{id}
      </select>
    

    元素 select 必須使用返回類型屬性 resultType 聲明查詢返回的結果類型,這裡為 tech.exchange.springboot.mybatis.model.MyRow,表示 MyBatis 會將查詢到的字段值按數據表列名一一映射到 MyRow 實例的各個字段;如果數據表列名與 MyRow 的字段名稱不一致,SELECT 語句可以使用別名(AS)重新定義列名:

        SELECT
          id,
          col1 AS col1,
          col2 AS col2
        FROM
          mytable
        WHERE
          id = #{id}
    

    接口方法

      /**
       * 查詢一行記錄
       *
       * @param id 記錄ID
       * @return 記錄
       */
      MyRow selectMyRow(int id);
    

    MyBatis 也是支持使用一個或多個基本類型變量傳遞參數的,注意名稱要對應保持一致。

    查詢一行記錄

        int id = 1;
        // 查詢記錄
        MyRow row = mapper.selectMyRow(id);
    
        System.out.println(row.getId());
        System.out.println(row.getCol1());
        System.out.println(row.getCol2());
    

    可以看到,這裡使用 insert 元素聲明的返回類型 MyRow 的實例變量接收查詢結果。

  2. 查詢多行記錄

    以查詢數據表 mytable 的全部記錄演示查詢多行記錄,也可以使用參數指定查詢條件,參數使用方法如上方所示,不再贅述。

    XML查詢元素

    <select id="selectMyRows" resultType="tech.exchange.springboot.mybatis.model.MyRow">
        SELECT
        *
        FROM
        mytable
      </select>
    

    查詢多行記錄,返回結果應該是一個類似 集合 的類型,但是 MyBatis 要求屬性 resultType 聲明的不是具體的集合類型(List),而是集合元素的類型,這裡仍是 tech.exchange.springboot.mybatis.model.MyRow。

    接口方法

      /**
       * 查詢多行記錄
       *
       * @return 多行記錄
       */
      List<MyRow> selectMyRows();
    

    查詢多行記錄

        List<MyRow> rows = mapper.selectMyRows();
    
        rows.forEach(System.out::println);
    

Update

修改數據表 mytable 指定主鍵ID的記錄。

XML修改元素

  <update id="updateMyRow">
    UPDATE
      mytable
    SET
      col1 = #{col1},
      col2 = #{col2}
    WHERE
      id = #{id}
  </update>

接口方法

  /**
   * 修改記錄
   *
   * @param row 一行記錄
   * @return 影響行數
   */
  int updateMyRow(MyRow row);

修改記錄

    MyRow row = new MyRow();
    
    // 指定ID
    row.setId(1);
    
    // 指定新的字段值
    row.setCol1("c");
    row.setCol2("d");

    // 修改
    int value = mapper.updateMyRow(row);
    System.out.println(value);

Delete

刪除數據表 mytable 指定主鍵ID的記錄。

XML刪除元素

  <delete id="deleteMyRow">
    DELETE FROM
    mytable
    WHERE
    id = #{id}
  </delete>

接口方法

  /**
   * 刪除記錄
   *
   * @param id 記錄ID
   * @return 影響行數
   */
  int deleteMyRow(int id);

刪除記錄

    int id = 1;

    int value = mapper.deleteMyRow(id);
    System.out.println(value);

小結

本文通過 SpringBoot 的命令行應用,演示 SpringBoot 和 MyBatis 的整體過程,以及實現基本 CRUD 的示例。

整體實踐下來,發現 MyBatis 的使用是有套路可循的,對於某一張數據表的讀寫操作:

  1. 創建一個或多個實體類,用於數據交互;
  2. 創建一個 MyBatis Mapper,用於封裝數據方法,Mapper 由兩部分組成:Interface(接口) + XML;
  3. Interface 中的每一個方法(Method)對應着 XML 中的一個元素(Element, insert/select/update/delete);
  4. MyBatis Mapper 方法的調用執行,本質就是 SQL 語句的執行。

受限於篇幅,只能討論 MyBatis 最基礎的內容,幫助大家入門,詳細內容請參考 MyTatis 官方文檔