LoginSignup
60
81

More than 1 year has passed since last update.

Spring Boot キャンプ : Spring Boot + MyBatis編

Last updated at Posted at 2019-07-11

Spring Bootキャンプシリーズ、Spring Boot + MyBatis編です。

今回の目的

Spring BootアプリでMyBatisを利用してDBアクセスを行います。

今回使用するライブラリ

  • spring-boot-starter:2.2.0.M4
  • mybatis-spring-boot-starter:2.0.1
  • mybatis-spring-boot-starter-test:2.0.1
  • h2:1.4.199
  • lombok:1.18.8

以降の手順のいくつかはSpring Initializrでプロジェクトを作成することにより解決されます。

DBアクセス

MyBatisの実装を行う前に、まずはDBアクセスするための設定を行います。

DBアクセスの定義

Spring BootではDBにアクセスするDataSource等のBean定義を自動的に行ってくれます。
デフォルトではインメモリのH2データベースにアクセスするため、依存関係にh2を追加する必要があります。

pom.xml

    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

Note.
Spring Bootの設定ファイルでドライバと接続先を変更すれば、別DBにアクセスできます。(依存関係に対応するドライバを追加する必要があります)

src/main/resources/application.yml

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/testdb
    username: postgres
    password: postgres

src/main/resources直下に以下のSQLファイルを作成すると、アプリ起動時にデータベースの初期化も自動的に行うこともできます。
デフォルトで認識されるSQLファイルは以下の通りです。

  • schema.sql
  • schema-${platform}.sql
  • data.sql
  • data-${platform}.sql

見たままですが、schema.sqlにはDDLを定義し、data.sqlにはDMLを定義します。

Note.
${platform}spring.datasource.platformプロパティで指定します。
単体テスト時はH2、結合テスト時はPostgresqlといった使い分けができそうですね。

今回はschema.sqlを作成してテーブルを作成します。

src/main/resources/schema.sql

create table if not exists todo (
    todo_id identity,
    todo_title varchar(30),
    finished boolean,
    created_at timestamp
);

mybatis-spring-boot-starter

Spring BootでMyBatisを利用するためのスターターです。
ぶっちゃけ詳細は開発者の@kazuki43zooさんの記事が詳しいので、そちらを見たほうが良いですw

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでMyBatisを使用するためのBean定義を自動的に行ってくれます。開発者は依存関係にmybatis-spring-boot-starterを追加するだけでOKです。

pom.xml

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

src/main/java/*/Application.java

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

メインクラスはSpring Bootアプリのデフォルトから何も変更していません。

Domainクラス

DBのデータをマッピングするドメインクラスは、普通のJava BeanでOKです。
以下では、GetterやSetterの実装を省略するため、Lombokの@Dataを利用しています。

src/main/java/*/domain/Todo.java

@Data
public class Todo {
    private String todoId;
    private String todoTitle;
    private boolean finished;
    private LocalDateTime createdAt;
}

Repositoryインターフェイス

SpringのRepositoryインターフェイスには、MyBatisのMapperを対応させます。
MyBatisのMapperは以下のいずれかの方法で実装することができます。

  • Repositoryインターフェイスに対応するMapper XMLを作成する
  • Repositoryインターフェイスに@Mapperを付与する

使いたいほうを使ってください!w
今回はアノテーションベースSpring Bootに合わせてMyBatisのMapperも@Mapperで実装します。

src/main/java/*/repository/TodoRepository.java

@Mapper // (1)
public interface TodoRepository {

    // (2)
    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId}")
    Optional<Todo> findById(String todoId);

    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
    Collection<Todo> findAll();

    @Insert("INSERT INTO todo (todo_title, finished, created_at) VALUES (#{todoTitle}, #{finished}, #{createdAt})")
    @Options(useGeneratedKeys = true, keyProperty = "todoId") // (3)
    void create(Todo todo);

    @Update("UPDATE todo SET todo_title = #{todoTitle}, finished = #{finished}, created_at = #{createdAt} WHERE todo_id = #{todoId}")
    boolean updateById(Todo todo);

    @Delete("DELETE FROM todo WHERE todo_id = #{todoId}")
    void deleteById(Todo todo);

    @Select("SELECT COUNT(*) FROM todo WHERE finished = #{finished}")
    long countByFinished(boolean finished);
}

(1) Repositoryインターフェイスに@Mapperを付与すると、MyBatisが自動的にスキャンしてMapperに登録してくれます。Repositoryインターフェイスはメインクラス配下のパッケージに置きましょう。

(2) メソッドに付与した@Select@Insert@Update@Deleteに、実行するSQLを実装します。SQL内の#{}で引数を利用していますが、同じファイル内に記載するのでXMLより分かりやすいですね。

(3) @Optionsは通常と異なる設定でSQLを実行する必要がある場合に付与します。ここではテーブルのキー項目がIDENTITY列のためDB側で自動採番されますが、@Optionsを利用することで自動採番されたIDを利用することができます。

複数行のSQL

上ではSQLを1行で記載していますが、可読性が悪いので複数行で記載することをお勧めします。
こんな感じで文字列結合する形になるので、XMLよりちょっと見にくいですね。

    @Select("SELECT"
            + " todo_id,"
            + " todo_title,"
            + " finished,"
            + " created_at"
            + " FROM todo"
            + " WHERE"
            + " todo_id = #{todoId}")
    Optional<Todo> findById(String todoId);

Select結果の自動マッピングと手動マッピング

例示したSelect文の結果カラム名とTodoクラスのプロパティ名が異なるため、Select結果はマッピングに失敗します。(todo_titletodoTitleなど)

これを解決するには以下のいずれかの方法があります。

  • MyBatisのネーミングルールによる自動マッピング
  • @Resultsおよび@ResultMapによる手動マッピング

個人的には、自動マッピングが利用できるようにプロパティ名を命名し、ルールから逸脱する場合のみ手動マッピングすることをお勧めします。

自動マッピング

アンダースコア区切りのカラム名とキャメルケースのプロパティ名が一致していれば、MyBatisのネーミングルールによる自動マッピングが可能です。

Spring Bootの設定ファイルに以下の設定を追加します。

src/main/resources/application.yml

mybatis:
  configuration:
    map-underscore-to-camel-case: true

Note.
mybatis.configuration.*プロパティでMyBatisの設定を変更することができます。設定可能なプロパティはMyBatis 3 - 設定を見てください。

手動マッピング

カラム名とプロパティ名が一致しない場合、SQLごとに@Resultsおよび@ResultMapによる手動マッピングを定義する必要があります。

  • @Resultsで手動マッピングルールを定義する
  • @Resultsで定義したルールを流用したければ、@ResultMap@ResultsのIDを指定する
    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId}")
    @Results(id = "todo", value = {
            @Result(column = "todo_id", property = "todoId"),
            @Result(column = "todo_title", property = "todoTitle"),
            @Result(column = "finished", property = "finished"),
            @Result(column = "created_at", property = "createdAt") })
    Optional<Todo> findById(String todoId);

    @Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
    @ResultMap("todo")
    Collection<Todo> findAll();

動的SQLと共通化

Mapper XMLで可能だった動的SQL(<if>)や共通化(<sql>)は、SqlProviderで実現します。

    // (2)
    @SelectProvider(type = TodoSqlProvider.class, method = "find")
    Optional<Todo> findById(String todoId);

    @SelectProvider(type = TodoSqlProvider.class, method = "find")
    Collection<Todo> findAll();

    // (1)
    public class TodoSqlProvider {
        public String find(String todoId) {
            return new SQL() {{
                SELECT("todo_id", "todo_title", "finished", "created_at");
                FROM("todo");
                if (todoId != null) {
                    WHERE("todo_id = #{todoId}");
                }
            }}.toString();
        }
    }

(1) SqlProviderクラスを実装して、メソッドでnew SQL()を利用してSQLを組み立てます。例ではインスタンスイニシャライザ(new SQL() {{ココ}})を利用していますが、もちろん普通にメソッドチェーンで書いても良いです。

Note.
WHERE句には引数を直接埋め込むこともできますが、必ず#{}を利用しましょう。
#{}はSQLインジェクションを防止してくれます。

(2) メソッドに@Selectの代わりに@SelectProviderを付与して、実装したSqlProviderクラスとメソッドを指定します。

Note.
インターフェイスのdefaultメソッドはSqlProviderメソッドにできません。必ずクラスを作る必要があります。
これはSqlProviderメソッドを利用する際、SqlProviderクラスのインスタンスが生成されるためです。

mybatis-spring-boot-starter-test

Spring BootでMyBatisをテストするためのスターターです。

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでMyBatisのMapperをテストするためのBean定義を自動的に行ってくれます。開発者は依存関係にmybatis-spring-boot-starter-testを追加して、少しの設定をするだけでOKです。

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>

JUnitテストケース

JUnitテストケースでは、クラスに@MyBatisTestを付与してテスト対象のリポジトリを@AutowiredするだけでOKです。

src/test/java/*/repository/TodoRepositoryTest.java

@MybatisTest
class TodoRepositoryTest {

    @Autowired
    TodoRepository todoRepository;

    @Test
    @Sql(statements = "INSERT INTO todo (todo_title, finished, created_at) VALUES ('sample todo', false, '2019-01-01')")
    void testFindAll() {
        // execute
        Collection<Todo> todos = todoRepository.findAll();

        // assert
        assertThat(todos)
                .hasSize(1)
                .extracting(Todo::getTodoTitle, Todo::isFinished, Todo::getCreatedAt)
                .containsExactly(tuple("sample todo", false, LocalDate.of(2019, 1, 1).atStartOfDay()));
    }

@MyBatisTestはMyBatisを利用するためのBean定義、DBにアクセスするDataSource等のBean定義を自動的に行ってくれますが、さらに@Transactionalを付与してくれます。

@Transactionalにより、@Sqlやテスト内で実行したSQLはテスト終了後にロールバックされます。インメモリではない実際のDBにアクセスしてテストするときもテストの独立性が保たれるので、安心ですね。

まとめ

MyBatisのスターターを利用することで、設定周りを省略してMapperの実装に注力することができました。設定の変更もSpring Bootの設定ファイルにプロパティを定義するだけなので、非常に楽ですね。

MapperをXMLで実装する場合はもう少し設定する必要がありそうですが、それはまたの機会に。

60
81
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
81