說明 當前的版本為 MyBatis 3.5.9 MyBatis Plus 3.5.1 Spring Boot 2.6.4 Postgresql 42.3.3 與 Spring Boot 結合使用 MyBatis 以下說明Spring Boot下完全以註解方式進行的配置, 覆蓋大部分功能場景 項目依賴 ...
說明
當前的版本為
- MyBatis 3.5.9
- MyBatis Plus 3.5.1
- Spring Boot 2.6.4
- Postgresql 42.3.3
與 Spring Boot 結合使用 MyBatis
以下說明Spring Boot下完全以註解方式進行的配置, 覆蓋大部分功能場景
項目依賴
需要以下的依賴, 版本由Spring Boot指定, 或者參考上面的版本號
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
配置
1.增加 MapperScan 註解
這個註解可以和@SpringBootApplication放在一起, 也可以放在單獨的一個 @Configuration
@SpringBootApplication
@MapperScan(basePackages = "com.yourdomain.demo.commons.impl.mapper")
public class CommonsBoot {
public static void main(String[] args) {
SpringApplication.run(CommonsBoot.class, args);
}
}
2. 準備POJO(PO或者DTO)
public class RoleItemDTO {
private Integer id;
private int roleId;
private int itemId;
private Date createTime;
// getters and setters
// ...
}
3. 準備Mapper
Mapper必須位於 @MapperScan 指定的包路徑, 其中 @Repository 用於指定 Bean 名稱, 內部用 @Select @Update @Delete @Insert 等編寫對應操作的SQL
@Repository("roleItemMapper")
public interface RoleItemMapper {
@Select("SELECT * FROM test_role_item WHERE role_id = #{roleId}")
List<RoleItemDTO> listByRoleId(@Param("roleId") int roleId);
}
4. 調用
測試用例
@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class RoleItemTest {
private static final Logger log = LoggerFactory.getLogger(RoleItemTest.class);
@Resource
private RoleItemMapper roleItemMapper;
@Test
@Order(1)
void insert() {
RoleItemDTO rc = new RoleItemDTO();
rc.setItemId(1);
rc.setRoleId(2);
rc.setCreateTime(new Date());
Assertions.assertThat(roleItemMapper.insert(rc)).isEqualTo(1);
List<RoleItemDTO> dummies = roleItemMapper.listByRoleId(2);
log.info(JacksonUtil.compress(dummies));
Assertions.assertThat(dummies).isNotNull();
}
}
註解使用
典型用例
public interface VillageMapper {
@Results({
@Result(property = "vid", column = "id"),
@Result(property = "villageName", column = "name"),
@Result(property = "district", column = "district")
})
@Select("SELECT id, name, district from village WHERE id = #{id}")
Village selectVillage(int id);
@Insert("INSERT into village(name,district) VALUES(#{villageName}, #{district})")
void insertVillage(Village village);
@Update("UPDATE village SET name=#{villageName}, district =#{district} WHERE id =#{vid}")
void updateVillage(Village village);
@Delete("DELETE FROM village WHERE id =#{id}")
void deleteVillage(int id);
}
傳參註解@Param
和${}
,#{}
在Mapper介面中, 使用@Param("paramName")
可以對傳入的參數進行命名, 在SQL中通過${paramName}
和#{paramName}
取值, 避免歧義.
${}
會在SQL字元串中直接替換, 非必要不建議使用這種方式#{}
是根據參數類型進行填充, 對於字元串會增加引號包圍, 是通常使用的傳參方式#{}
支持對象傳入, 在引用時通過#{obj.variable}
的方式取值, 參考下麵 Insert 的例子
Select和結果集映射
對於普通類型例如primitive類型, 數值(Integer, Long, Double, BigDecimal等), 字元串String, 日期Date, MyBatis都已經做了自動轉換不需要手工設定. 對於特殊類型, 例如對應Postgresql的 Array, Jsonb, 需要使用註解 @Results 進行指定, 在註解中, 對於已經有預設handler的欄位可以省略, 只需要添加特殊指定hanler的欄位.
Array類型欄位的例子, 對templates欄位的轉換進行指定
// POJO定義
public class ViewPO implements Serializable {
private Integer id;
private String name;
private Integer[] templates;
}
// Mapper方法
@Results({
@Result(property = "templates", column = "template_ids", typeHandler = ArrayTypeHandler.class)
})
@Select("select * from test_view where id = #{id}")
ViewPO select(@Param("id") int id);
JSONB類型欄位的例子, 對ext欄位的轉換進行指定
// POJO定義
public class AccountDTO {
private Long id;
private String name;
private int val;
private BigDecimal amount;
private Ext ext;
public static class Ext {
private Integer id;
private String name;
private BigDecimal level;
// getters and setters
}
// getters and setters
}
// Mapper方法
@Results({
@Result(property = "ext", column = "ext", typeHandler = JacksonTypeHandler.class)
})
@Select("select * from test_account where amount > #{amountGt}")
List<AccountDTO> list(@Param("amountGt") BigDecimal amountGt);
Insert和自增主鍵
@Insert("INSERT INTO role_item (role_id, item_id, create_time) VALUES (#{po.roleId}, #{po.itemId}, #{po.createTime})")
@Options(useGeneratedKeys=true, keyProperty="id")
int insert(@Param("po") RoleItemPO po);
Delete
@Delete("DELETE FROM role_item WHERE item_id = #{itemId}")
int deleteByItemId(@Param("itemId") int itemId);
Update
@Update("update widget set name=#{po.name}, manufacturer=#{po.manufacturer} where id=#{id}")
public void updateById(@Param("id") int id, @Param("po") Widget po);
與 Spring Boot 結合使用 MyBatis Plus
MyBatis Plus 相對於原生 MyBatis, 增加了對常用CRUD方法的包裝, 減小了手工編寫SQL的工作量. MyBatis Plus 對多主鍵的表支持不太好, 如果資料庫中的某個數據表使用了聯合主鍵, 建議使用原生的 MyBatis 註解對這個數據表進行操作
項目依賴
將 MyBatis 的依賴替換為MyBatis Plus, 參考上面的版本號, 本文使用的是3.5.1
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
配置
1.增加 MyBatisPlus 的配置類
因為除了設置 MapperScan, 還要設置分頁插件, 因此放到了單獨的 @Configuration
@Configuration
@MapperScan(basePackages = "com.yourdomain.demo.common.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
return interceptor;
}
}
2. 準備POJO(PO或者DTO)
如果使用 MyBatis Plus, 常用的三個註解為
- @TableName 表名, autoResultMap設置為true, 下麵 ext 欄位的 typeHandler 才會生效
- @TableId 唯一主鍵, 這個註解在當前類中只允許出現一次. 唯一主鍵修飾的id欄位需要使用包裝類, 例如 Integer或Long, 在Insert時這個欄位賦值NULL, 才能正確獲取生成的自增ID值
- @TableField 欄位映射關係, 當欄位名與變數名不能直接對應時使用, 當類型轉換需要特殊處理時也需要使用這個註解
@TableName(value = "test_users", autoResultMap = true)
public class UserPO implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String userName;
private String password;
@TableField("creat_time")
private Date createdAt;
@TableField("update_time")
private Date updatedAt;
@TableField(typeHandler = JacksonTypeHandler.class)
private JsonNode ext;
}
3. 準備 Mapper
對於 MyBatis Plus, 最常見的 Mapper 初始化方式為擴展 BaseMapper, 這個介面已經實現了 selectById, selectList, insert, delete, deleteById, updateById 等常用方法
@Repository("userMapper")
public interface UserMapper extends BaseMapper<UserPO> {
}
註意
- 如果 BaseMapper 自帶方法不能滿足功能需求, 可以通過 @Select, @Update, @Insert, @Delete 等註解添加自定義介面, 實際上就是按原生 MyBatis 的方式處理.
- 如果添加了 @Select 查詢並且結果也是標準POJO, 結果中的 JSONB 類型欄位並不會自動賦值(輸出為null). 如果需要轉換, 需要按原生 MyBatis 一樣在方法上添加 @Results 註解.
4. 調用
調用方式與原生 MyBatis 一致
註解使用
MyBatis Plus 的常用註解主要有兩個, 一個是 Wrapper(有多個實現類), 一個是 Page, 分別用於查詢條件和分頁條件.
QueryWrapper 用於條件,排序和聚合
QueryWrapper 自帶了常用的邏輯方法 eq(相等), ne(不相等), lt/gt(小於/大於), lte/gte(小於等於/大於等於), like(兩邊%), likeLeft(左%), likeRiht(右%), exists(條件子查詢), in(數組中包含), 以及 groupBy, orderBy 方法
- 三參數格式中, 第一個參數為本條件是否加入查詢的條件判斷, 便於根據輸入進行條件組合
- 三參數的第二參數(或二參數的第一參數), 其值為資料庫的欄位名, 這點與 LambdaQueryWrapper 不同, 需要註意
List<FieldPO> fields = fieldMapper.selectList(new QueryWrapper<FieldPO>().eq("t_id", tId).orderByAsc("id"));
一個複雜一點的例子, 註意最後一個exists例子中的傳參方式
QueryWrapper<ItemPO> wrapper = new QueryWrapper<ItemPO>()
.eq(name != null, "item_name", name)
.like(nameLike != null, "item_name", nameLike)
.like(descLike != null, "desc", descLike)
.eq(type > 0, "item_type", type)
.eq(state > 0, "item_state", state)
.eq(creatorId > 0, "creator_id", creatorId)
.in(idIn != null && idIn.size() > 0, "id", idIn)
.exists(roleId > 0, "select 1 from role_item where role_item.item_id=item.id and role_item.role_id = {0}", roleId);
LambdaQueryWrapper POJO欄位方式
LambdaQueryWrapper 與 QueryWrapper 的區別在與條件參數為POJO的取值方法名而不是資料庫欄位, 從設計上看這樣可以完全隔離資料庫層的信息, 是更好的一種實現, 例子
LambdaQueryWrapper<AccountDTO> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.likeRight(AccountDTO::getName , "acc").lt(AccountDTO::getVal , 40).last("limit 5");
但是當前 MyBatis Plus 的實現在高版本JDK上會提示警告, 在 JDK9+ 上運行時會提示An illegal reflective access operation has occurred
#issue. 建議還是繼續用 QueryWrapper, 待這部分的實現修複後再遷移.
Page 分頁
列表的排序和聚合都是通過Wrapper指定的, Page 用於列表中的分頁. 前提: 在 Configuration 中, 通過 MybatisPlusInterceptor 添加 PaginationInnerInterceptor, 參考前面的配置部分, 需要配置正確的 DbType.
通過頁數和每頁記錄數創建Page對象
Page page = new Page<>(vo.getPageNum(), vo.getPageSize());
結合Wrapper進行查詢
@Override
public List<ItemPO> listByArgs(
Page<ItemPO> page,
String name,
int type) {
QueryWrapper<ItemPO> wrapper = new QueryWrapper<ItemPO>()
.eq(name != null, "item_name", name)
.eq(type > 0, "item_type", type);
return itemMapper.selectPage(page, wrapper).getRecords();
}
如果需要總數, 可以直接返回selectPage(page, wrapper)
的結果.
混合使用 MyBatis 原生方式和 MyBatis Plus
因為 MyBatis Plus 是對 MyBatis 功能的包裝, 底層依然是 MyBatis, 所以在使用 MyBatis Plus 的過程中, MyBatis 的原生方法依然可用.
例如, 可以在原生方法中, 使用 MyBatis Plus 的 Wrapper
@Repository("userMapper")
public interface UserMapper extends BaseMapper<UserPO> {
@Select("select * from u_users ${ew.customSqlSegment}")
List<UserPO> selectAll(@Param(Constants.WRAPPER) Wrapper<UserPO> wrapper);
}
其它
批量執行
批量執行可以通過兩種方式,
MyBatis 原生環境
在 MyBatis 原生環境下, 可以通過獲取 sqlSession 後手工發起
@Resource(name = "sqlSessionFactory")
private SqlSessionFactory sqlSessionFactory;
public void addItems(List<ItemPO> items) {
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try {
for (ItemPO item : items) {
itemMapper.insert(item);
}
session.commit();
session.clearCache();
} catch (Exception e) {
log.error("Exception in batch update", e);
session.rollback();
} finally {
session.close();
}
}
MyBatis Plus 的 saveBatch
在 MyBatis Plus 中, 可以通過擴展 ServiceImpl<Mapper, POJO> 類, 直接使用其中的 saveBatch() 方法
public class FieldServiceImpl
extends ServiceImpl<FieldMapper, FieldPO>
implements FieldService {
@Override
public Result saveSomething(List<FieldPO> pos) {
// 在方法中使用 saveBatch
saveBatch(pos);
}
}
動態表名
如果需要在查詢中指定表名, 最簡單的方式是通過${}
方式傳參, 例子如下
@Select("SELECT * FROM ${tableName} WHERE item_type=#{itemType} " +
"AND item_name=#{itemName} AND item_id=#{itemId} AND label=#{label}")
LabelMapDTO selectFromTable(
@Param("tableName") String tableName,
@Param("itemType") int itemType,
@Param("itemName") String itemName,
@Param("itemId") String itemId,
@Param("label") String label);
@Select("SELECT * FROM ${tableName} WHERE item_type=#{itemType} " +
"AND item_name=#{itemName} AND item_id=#{itemId}")
List<LabelMapDTO> selectListFromTable(
@Param("tableName") String tableName,
@Param("itemType") int itemType,
@Param("itemName") String itemName,
@Param("itemId") String itemId);