紀錄Spring Boot整合Mybatis筆記以及遇到要注意的事項。
Mybatis有annotation
以及xml
兩種方式,此處為套用 xml方式。
1. 在pom.xml中新增dependency
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
2. 配置 Mapper.java
範例:有個Table名稱為test_table,其中PK是DB有設定Auto increment,因此新增時不用給PK。
CREATE TABLE `test_table` (
`id` bigint(20) PRIMARY KEY AUTO_INCREMENT
`test_amount` decimal(10,2) NOT NULL
);
Mapper.java interface中可以宣告需要用到的方法 (方法名稱會對應到之後 xml 的設定)
package com.demo.repository.mapper;public interface TestTableMapper {
/**
* 新增 Test 資料
*
* @param testTable 新增資料
* @return 新增資料影響的行數
*/
long insertTestData(TestTable testTable);
/**
* 查詢特定區間紀錄
*
* @param id Table PK
* @param amount 金額
* @return 查詢結果
*/
List<TestTable> findTestDataByCondition(
@Param("id") long id,
@Param("test_amount") long amount;
}
- 備註1:要注意的是Mybatis這邊insertData所返回的值並不是新增後的 PK,返回的值為此次「新增資料所影響的行數」,至於要怎麼取得generate的PK稍後解說。
- 備註2:很多文章會在此處每個interface上加上@Mapper宣告此類別為Mapper,但其實效果等同於下方配置3的設定,所以若已經加上MapperScan便不需要再綴上此annotation。
3. 配置設定在 Application 中 (啟動類別)
新增 @MapperScan 在class上,其中basePackages宣告為步驟2中配置,讓Spring Boot 啟動時可以知道Mapper放置在哪。
@SpringBootApplication
@MapperScan(basePackages = {"com.demo.repository.mapper"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3. 配置 Mapper.xml
基本的CRUD網路上資源很多
此處範例是將Mapper.xml配置在 src/resources/ 下方,其中SQL說明:
- 新增一筆資料
- 查詢多筆資料(其中查詢條件非必填),如果有傳id則id=傳入id,如果有傳amount則條件查詢test_amount > 傳入的 amount
<?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="package com.demo.repository.mapper.TestTableMapper">
<insert id="insertTestData" parameterType="com.demo.repository.model.TestTable"
useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into test_table(id, test_amount)
values(#{id}, #{amount})
</insert> <resultMap id="testResultMap" type="com.demo.repository.model.TestTable">
<id column="id" property="id"/>
<result column="test_amount" property="amount"/>
</resultMap> <select id="findTestDataByCondition" resultMap="testResultMap">
select * from transaction_log
<where>
<if test="id != 0">
id = #{id}
</if>
<if test="amount != null">
test_amount > #{amount}
</if>
</where>
</select>
</mapper>
<mapper namespace=”XXX”>
此處XXX為對應的Mapper.java路徑
<insert id=”insertTestData” parameterType=”com.demo.repository.model.TestTable”
useGeneratedKeys=”true” keyProperty=”id” keyColumn=”id”>
- id: 為對應的為Mapper.java中的方法名稱
- parameterType: 宣告傳入的parameter類型
- useGeneratedKeys=”true”: 此設定代表PK為Auto Generate
- keyProperty: 對應Model中的PK名稱
- keyColumn: 對應Table中的PK名稱
<resultMap>
若資料可能會返回多筆 (例如: List<Object>形式)
需要宣告resultMap這段來定義返回值,其中用id當作辨別給select使用。
<select id=”findTestDataByCondition” resultMap=”testResultMap”>
- id: 為對應的為Mapper.java中的方法名稱
- resultMap: 資料返回時對應的resultMap id
- 若想要動態配置查詢的where條件,可使用tag <where> / <if>
- 可以看到大於或小於等含有角括號的SQL,要用 > 或 < 等字元替代
4. 配置 application.properties (or yml)
這邊值得注意的是,當xml檔案所在的package名稱和interface對應的package名稱沒有對應時,會出現如下錯誤org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
因此在application.properties中,我們需要宣告mybatis.mapper-locations來表示實際上xml位置,範例如下:
mybatis.mapper-locations=classpath*:*Mapper.xml
取得 Auto Increment Key 新增的值
差點忘記補充要如何取得insert資料後新Generate的key值
其實會直接更新在原先傳入的Model中,因此程式中取得方式如下
TestTable testTable = new TestTable();
testTable.setAmount(5000);mapper.insertTestData(testTable); //此時呼叫Mapper.java中方法
long id = testTable.getId(); //再次取得的就會是新增的id
大致上Mybatis使用時xml部分設定就這樣:)
- Mapper.xml 更詳細tag使用說明也可以參考官方說明: https://mybatis.org/mybatis-3/sqlmap-xml.html
Auto Increment Key 單測時如何撰寫
假設有一User table有三個欄位(id, name, age) ,其中id是自動遞增的key,今天service中有一個方法會需要取得insert後的id做其他事。
public ResultVO testMethod() {
User user = this.insertData(name, age);
long id = user.getId();
// do something...
}private User insertData(String name, int age) {
User user = new User();
user.setName(name);
user.setAge(age);
mapper.insertTestData(user);
return user;
}
此時在撰寫Unit Test可使用Answer去mock自動產生的id
Mockito.when(mapper.insert(Mockito.any(User.class)))
.thenAnswer((Answer<Integer>) invocation -> {
User record = invocation.getArgument(0, User.class);
record.setId(5);
return 1;
});
這樣就不會發生NPE的狀況囉