MyBatis-Plusを使って見た

[KINTO Technologies Advent Calendar 2021 - Qiita](https://qiita.com/advent-calendar/2021/kinto-technologies) の9日目の記事です。
# Mybatis-Plusとは

**　　MyBatis-Plus is an powerful enhanced toolkit of MyBatis for simplify development. This toolkit provides some efficient, useful, out-of-the-box features for MyBatis, use it can effectively save your development time.**

***　　MyBatis-Plusは、開発を簡素化するためのMyBatisの拡張です。この拡張ライブラリはMyBatisをもっと効率的で便利な機能を提供します。これを使うと、開発時間を効果的に節約できます。※1***
<img width="400" alt="Mybatis-Plus" align="center" src="https://baomidou.com/img/logo.svg">


　　要するMybatisの拡張で、Mybatisの機能をさらに使いやすく、効率化を目的とするライブラリである。現状日本語の資料が少なかったので特徴、使い方についてまとめて見ました。

#主要機能&特徴
- **非侵襲的（ひしんしゅうてき）**　あくまで拡張であり、既存のMybatisを採用したプロジェクトに導入しても問題ない。
- **性能ロスが少ない**　起動時CRUD SQLを自動的にInjectする。
- **強力なCRUD**　非常に少ない設定、コードでMapper/Serviceの中でCRUD機能を実現できる。シングルテーブルに対するCRUDに関してはXMLファイルは必要ありません。
- **lambda式を使って簡単にカスタムクエリ**
-  **多数のデータベースのサポート**
　Support MySQL, MariaDB, Oracle, DB2, H2, HSQL, SQLite, PostgreSQL, SQLServer2005, SQLServer, etc.
- **主キー値の自動生成**　(4種類の生成方法をサポート)
- **ActiveRecord**　モードのサポート Entityクラスからinsert,delete,update操作ができる。
- **グローバルメソッドのカスタマイズ機能**　備え付きのCRUDメソッド以外に独自のメソッドを追加し、任意のMapperから呼び出せる。
- **コードの自動生成**　mapper, model, service, controllerのソースを自動生成。
- **ページング機能を提供**　Paging plug-inを使って簡単にページング処理ができる。（MySQL, MariaDB, Oracle, DB2, H2, HSQLDB, SQLite, PostgreSQL, SQLServer）
- **パフォーマンスInterceptor機能**　SQLの性能情報をモニタリング可能。
- **Smart Interceptor**　全テーブル更新、全テーブル削除を検知し、操作ミスを防ぐ。
- **Sql Injection Interceptor**　SQLインジェクション防止機能。

#Mybatis-Plusの構成
![Qiita](https://baomidou.com/img/mybatis-plus-framework.jpg "Mybatis-Plus Structure")

#使い方

　ここではSpringBootのRestControllerを通じてデータのやり取りを行います。

#実行環境
- OS:Windows10
- DB:MySQL5.7.27
- Java:11
- SpringBoot:2.6.1
- Mybatis:3.5.7
- Mybatis-Plus:3.4.3.4

## サンプルDB

``` sql
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT 'Primary key',
  `name` varchar(30) DEFAULT NULL COMMENT 'name',
  `age` int(11) DEFAULT NULL COMMENT 'age',
  `email` varchar(50) DEFAULT NULL COMMENT 'Email',
  `area_cd` int(11) DEFAULT NULL COMMENT 'Area',
  `city_cd` int(11) DEFAULT NULL COMMENT 'City',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `city` (
  `area_cd` int(5) NOT NULL DEFAULT '0' COMMENT 'Area',
  `city_cd` int(5) NOT NULL DEFAULT '0' COMMENT 'City',
  `name` varchar(50) DEFAULT NULL COMMENT 'CityName',
  PRIMARY KEY (`area_cd`,`city_cd`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `city` VALUES (1,1,'tokyo'),(1,2,'yokohama'),(1,3,'chiba'),(2,1,'kyoto'),(2,2,'osaka'),(2,3,'nagoya'),(3,1,'sapporo'),(3,2,'otaru'),(4,1,'hukuoka'),(4,2,'kumamoto');

INSERT INTO `user` VALUES (1,'Jone',18,'test1@sample.com',1,1),(2,'Jack',20,'test2@sample.com',1,2),(3,'Tom',28,'test3@sample.com',3,1),(4,'Sandy',21,'test4@sample.com',2,1),(5,'Billie',24,'test5@sample.com',4,2);

```

## Mavenプロジェクト作成

``` xml:pom.xml
<?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.6.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>mybatis.plus</groupId>
	<artifactId>sample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sample</name>
	<description>MybatisPlus Sample project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.3.4</version>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-extension</artifactId>
			<version>3.4.3.4</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</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>

```

##ソースフォルダ構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83580/34f82e8e-3de7-c3c3-356a-6cdf98954515.png)



##DB環境設定

```yml:application.yml
spring:
  datasource:
    url:  jdbc:mysql://localhost:3306/sample?characterEncoding=utf-8&useSSL=false
    username: sample
    password: password
    driverClassName: com.mysql.jdbc.Driver
    testOnBorrow: true
    validationQuery: SELECT 1 FROM DUAL
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      connection-timeout: 60000
      connection-test-query: SELECT 1 FROM DUAL
      minimum-idle: 5
      maximum-pool-size: 10
```

## mapperクラスのパッケージを指定
```java:SampleApplication
@SpringBootApplication
@MapperScan("mybatis.plus.sample.mapper")
public class SampleApplication {
	public static void main(String[] args) {
		SpringApplication.run(SampleApplication.class, args);
	}
}
```

##データソース周り設定
```java:MybatisPlusConfig.java
package mybatis.plus.sample.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
public class MybatisPlusConfig {
    @Autowired
    private DataSource dataSource;
    @Bean
    @ConfigurationProperties(prefix = "mybatis-plus")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{mybatisPlusInterceptor()});
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // MyBatisのSQL文格納XMLファイルの場所指定
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor pi = new PaginationInnerInterceptor(DbType.MYSQL);
        // 1ページのデータ数を制限（1ページの取得データ数は都度設定する）
        pi.setMaxLimit(500L);
        // 最後のページを超えた場合true:1ページ目に戻る、false:処理を継続
        pi.setOverflow(false);
        interceptor.addInnerInterceptor(pi);
        return interceptor;
    }
}

```
##EntityとVOの定義
```java:UserEntity.java
package mybatis.plus.sample.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("user")
public class UserEntity {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableField("area_cd")
    private Integer areaCd;
    @TableField("city_cd")
    private Integer cityCd;
}
```

```java:CityEntity.java
package mybatis.plus.sample.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
@TableName("city")
public class CityEntity {
    @TableField("area_cd" )
    private Integer areaCd;
    @TableField("city_cd")
    private Integer cityCd;
    private String name;
}
```
###テーブル結合した場合のデータ格納
```java:UserCityVo.java
package mybatis.plus.sample.vo;
import lombok.Data;

@Data
public class UserCityVo {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Integer areaCd;
    private Integer cityCd;
    private String cityName;
}
```

##MapperのあたりはSpringDataJPAに似ていますね

```java:UserMapper.java
package mybatis.plus.sample.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import mybatis.plus.sample.entity.UserEntity;
import mybatis.plus.sample.vo.UserCityVo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<UserEntity> {
    List<UserEntity> selectUserAgeGreaterThan(Integer age);
    IPage<UserCityVo> selectUserCity(Page<?> page);
    IPage<UserCityVo> selectUserCity(Page<?> page, Integer age);
}
```
```xml:UserMapper用SQLのXML定義user.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "../dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.plus.sample.mapper.UserMapper">
	<select id="selectUserAgeGreaterThan" resultType="mybatis.plus.sample.entity.UserEntity">
		SELECT * FROM user where age &gt;= #{age}
	</select>
	<select id="selectUserCity" resultType="mybatis.plus.sample.vo.UserCityVo">
		SELECT u.*,c.name as cityName FROM user as u left join city as c
		on u.area_cd = c.area_cd and u.city_cd = c.city_cd where u.age &gt;= #{age}
	</select>
</mapper>
```

```java:CityMapper.java
package mybatis.plus.sample.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import mybatis.plus.sample.entity.CityEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CityMapper extends BaseMapper<CityEntity> {}
```


##サービス層の定義
```java:CityService.java
package mybatis.plus.sample.service;
import com.baomidou.mybatisplus.extension.service.IService;
import mybatis.plus.sample.entity.CityEntity;
public interface CityService extends IService<CityEntity> {
    int updateCity(CityEntity city);
}
```
```java:CityServiceImpl.java
package mybatis.plus.sample.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import mybatis.plus.sample.entity.CityEntity;
import mybatis.plus.sample.mapper.CityMapper;
import org.springframework.stereotype.Service;
import java.util.HashMap;
@Service
public class CityServiceImpl extends ServiceImpl<CityMapper, CityEntity> implements CityService {
    @Override
    public int updateCity(CityEntity city) {
        // 条件指定はQueryWrapperを使います。
        return baseMapper.update(city, new QueryWrapper<CityEntity>().allEq(new HashMap<>(){{
            put("area_cd", city.getAreaCd());
            put("city_cd", city.getCityCd());
        }}));
    }
}
```
```java:UserService.java
package mybatis.plus.sample.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import mybatis.plus.sample.entity.UserEntity;
import mybatis.plus.sample.vo.UserCityVo;
import java.util.List;
public interface UserService extends IService<UserEntity> {
    List<UserEntity> getUserList();
    List<UserEntity> getUserList(Integer age);
    IPage<UserCityVo> getUserCityList(Page<UserCityVo> page, Integer age);
    UserEntity getUser(Long id);
    int addUser(UserEntity user);
    int updateUser(UserEntity user);
    int deleteUser(Long id);
}
```
```java:UserServiceImpl.java
package mybatis.plus.sample.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import mybatis.plus.sample.entity.UserEntity;
import mybatis.plus.sample.mapper.UserMapper;
import mybatis.plus.sample.vo.UserCityVo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {
    private final UserMapper userMapper;
    public List<UserEntity> getUserList() {
        return userMapper.selectList(null);
    }
    public List<UserEntity> getUserList(Integer age) {
        return userMapper.selectUserAgeGreaterThan(age);
    }
    @Override
    public IPage<UserCityVo> getUserCityList(Page<UserCityVo> page, Integer age) {
        if(age == null) {
            return userMapper.selectUserCity(page);
        }
        return userMapper.selectUserCity(page, age);
    }
    public UserEntity getUser(Long id) {
        return userMapper.selectById(id);
    }
    public int addUser(UserEntity user) {
        return userMapper.insert(user);
    }
    public int updateUser(UserEntity user) {
       return userMapper.updateById(user);
    }
    @Override
    public int deleteUser(Long id) {
        return userMapper.delete(new QueryWrapper<UserEntity>().eq("id", id));
    }
}
```
##エンドポイント定義
```java:UserController.java
package mybatis.plus.sample.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import mybatis.plus.sample.entity.UserEntity;
import mybatis.plus.sample.service.UserService;
import mybatis.plus.sample.vo.UserCityVo;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService service;
    @GetMapping("/users")
    public List<UserEntity> getUserListByAge(@RequestParam(value = "age", required = false) String age) {
        if(StringUtils.isEmpty(age)) {
            return service.getUserList();
        }
        return service.getUserList(Integer.valueOf(age));
    }
    @GetMapping("/users/page")
    public IPage<UserCityVo> getUserCityPageByAge(
            @RequestParam(value = "age", required = false) String age,
            @RequestParam(value = "page", required = false) Integer page) {
        Page<UserCityVo> pageObj = new Page<>();
        pageObj.setCurrent(page);
        // 1ページの取得データ数
        pageObj.setSize(3);
        if(StringUtils.isEmpty(age)) {
            return service.getUserCityList(pageObj, null);
        }
        return service.getUserCityList(pageObj, Integer.valueOf(age));
    }
    @GetMapping("/user")
    public UserEntity getUser(@RequestParam("id") Long id) {
        return service.getUser(id);
    }
    @PutMapping("/user")
    public void addUser(@RequestBody UserEntity user) {
        service.addUser(user);
    }
    @DeleteMapping("/user/{id}")
    public void deleteUser(@PathVariable Long id) {
        service.deleteUser(id);
    }
    @PostMapping("/user/{id}")
    public void updateUser(@PathVariable(value = "id", required = false) String id,
            @RequestBody UserEntity user) {
        service.updateUser(user);
    }
}
```
```java:CityController.java
package mybatis.plus.sample.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import mybatis.plus.sample.entity.CityEntity;
import mybatis.plus.sample.service.CityService;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class CityController {
    private final CityService service;
    @GetMapping("/cities")
    public List<CityEntity> getAllCity() {
        return service.list();
    }
    @GetMapping("/city")
    public CityEntity getOneCity(@RequestParam("area") Integer area, @RequestParam("city") Integer city) {
        return service.getOne(new QueryWrapper<CityEntity>().allEq(new HashMap<>(){{
            put("area_cd", area);
            put("city_cd", city);
        }}));
    }
    @PutMapping("/city")
    public void addCity(@RequestBody CityEntity city) {
        service.save(city);
    }
    @PostMapping("/city")
    public void updateCity(@RequestBody CityEntity city) {
        // service.saveOrUpdate(city); Error! 複合主キーによる更新ができない
        service.updateCity(city);
    }
    @DeleteMapping("/deletecity/{area}/{city}")
    public void deleteCity(@PathVariable("area") Integer area, @PathVariable("city") Integer city) {
        service.remove(new QueryWrapper<CityEntity>().allEq(new HashMap<>(){{
            put("area_cd", area);
            put("city_cd", city);
        }}));
    }
}

```

　　実際エンドポイントに対してアクセスすると一覧取得、1レコード取得、ページ指定一覧取得、更新、削除が一通りできました。実際サービスクラスとMapperクラスの中身をほとんど書かなくても、基本的なCRUDができてしまうのでかなり効率的ではないかと思います。
##一覧取得結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83580/4407d766-ead3-05f0-edc3-2a09d141cca0.png)

##ページングとテーブル結合取得結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83580/d6e939c6-9b72-2006-aba2-74aea933b0ee.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83580/128b19cb-fe02-89e2-f10f-7c91d852878f.png)



#いいところ

- **Interfaceのみ定義することでCRUDができる**
- **mybatis-config.xmlが不要**
- **ServiceのCRUDができる（ServiceのImplクラスを書かなくてもいい）**
- **XMLを定義して直接XMLを書くこともできる。（Mybatisと同時に使える）**
- **ページング処理が楽**
 

#気になる点

- 複合主キーによるCRUDのサポートが弱い。
- 主キーがないテーブルに対する操作が一部制限されている。
- 複数テーブルのJOINはXMLを書く必要がある。
- ドキュメントが少ない（クオリティもいまいちでした）



#まとめ
　　少ないコードで基本的なCRUDを実現することができて開発効率がかなり上がるのではないかと思います。一方でテーブルの主キーの定義によって一部利用制限があったり、複数テーブルをJOINが気軽にできないところがすこし残念なところでした。逆に言えばそこさえ改善できればもっと強力になるのは間違いないと思います。Spring Data JPAと比較するとMapperの定義のみでCRUDができる点はよく似ていますが、そこで更にServiceに対してもCRUDメソッドの拡張を提供しており、シンプルなDBアクセスならすぐ使えるところが魅力的ではないでしょうか。


#参考資料
- ※1 https://baomidou.com/en/guide/#features
- ※2 https://github.com/baomidou/mybatis-plus
- https://developpaper.com/springboot-integrates-mybatis-plus/

当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
[KINTO Technologies コーポレートサイト](https://www.kinto-technologies.com/)