はじめに
SpringBootでDB操作を行うRepository層の実装をまとめていきたいと思います。
DBとのインタフェースに使うO/Rマッパーはいろいろありますが、この記事ではMyBatis
を使います。
MyBatis
はJavaとSQLのマッピングがシンプルでわかりやすく動的SQLの記述も可能です。
MyBatisGeneratorを使うとDBのテーブル情報から、JavaのModelクラスやCRUD用メソッドが実装されたMapperクラスをごそっと作れるのがとても便利です。(別の記事で紹介予定)
今回はModelとMapperの対応を理解するため手動で作成します。
開発環境
OS : macOS Catalina
IDE : IntelliJ Ultimate
Java : 11
Gradle : 6.6.1
SpringBoot : 2.3.4
MySQL : 8.0.17
1. データベースの準備
まずはアプリケーションで操作するDBの準備を行います。
ここではMySQL
のローカルの環境を整えます。
1-1. MySQLの準備
DockerでMySQLを使うのがお手軽で簡単です。(丸投げ)
上記でMySQLにroot
ユーザで接続後、本アプリ用のdatabase
とuser
を作っておきます。
-- データベースdemo_dbの作成
CREATE DATABASE demo_db;
-- ユーザ名:demo、パスワード:demo、権限:demo_dbに対する全ての権限
CREATE USER 'demo'@'%' IDENTIFIED BY 'demo';
GRANT ALL PRIVILEGES ON demo_db.* TO 'demo'@'%';
FLUSH PRIVILEGES;
これでMySQLの準備は完了です。
1-2. SpringBootプロジェクトの作成
SpringBootプロジェクトは前回のをベースにMyBatis、JDBC(MySQL)のライブラリを追加します。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3' // 追加
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.22' // 追加
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
1-3. アプリ用のデータソースの設定
アプリのローカル環境用にMySQL
への接続設定をmain/resources/application.yml
に記述します。
先ほど作った user:demo
、Database:demo_db
の接続設定は以下の通りです。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_db
username: demo
password: demo
mybatis:
configuration:
map-underscore-to-camel-case: true
map-underscore-to-camel-case: true
はDBオブジェクトとJavaオブジェクトをマッピングする際にスネークケースの名前をキャメルケースにマッピングしてくれます。
以上でMySQL
の接続設定は完了です。
1-4. テーブルの作成
簡単なサンプルのテーブルを作成します。
customerテーブル仕様
列名 | 型 | 制約など |
---|---|---|
id | int | 自動採番のプライマリキー |
name | varchar(100) | not null制約 |
age | int | not null制約 |
address | varchar(200) |
DDL
create table customer
(
id int auto_increment,
name varchar(100) not null,
age int not null,
address varchar(200) null,
constraint customer_pk primary key (id)
);
DML
ついでに初期投入データも作っておきます。
insert into
customer (name, age, address)
VALUES
('ルーク・スカイウォーカー', 19, 'タトゥイーン'),
('レイア・オーガナ', 19, 'オルデラン'),
('ハン・ソロ', 32, 'コレリア'),
('ダース・ベイダー', 41, 'タトゥイーン');
上記のDDL、DMLをMySQL
で実行&commitしてテーブルと初期データをセットアップします。
また、作ったDDLはschema.sql
、DMLはdata.sql
としてtest/resources
に保存しておきます。
これらのファイルは次回のDBUnitテストで使います。
├── build.gradle
└── src
├── main
└── test
├── java
└── resources
├── application.yml
├── schema.sql // DDL
└── data.sql // DML
2. MyBatisを使ったRepositoryの作成
DB操作を行うためのRepositoryクラスを作成します。
今回、O/RマッパーにMyBatisを使うため、お作法に則り
- Select句の結果にマッピングするModel(Entity)クラスの作成
- ModelとCRUDのSQLをマッピングするMapperの作成
- Mapperを利用するRepositoryクラスの作成
の順に作っていきます。
2-1. Model(Entity)クラスの作成
先ほど作ったcustomer
テーブルの1行分のデータを保持するModelクラスを作成します。
package com.example.dbunitdemo.domain.model;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class Customer {
private Long id;
private String name;
private Integer age;
private String address;
}
@lombok.Data
のおかげでアクセサとtoString()が自動で作られるのでシンプルです。
2-2. Mapperの作成
先ほど作ったCustomer
クラスをCRUDのSQLと結びつけるMapperを作成します。
MapperはJavaのインタフェース
とSQLを記述したXMLファイル
の2つで1組みとなるよう作成します。
CustomerMapper.java
package com.example.dbunitdemo.domain.mapper;
import com.example.dbunitdemo.domain.model.Customer;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface CustomerMapper {
List<Customer> findAll();
Customer get(@Param("id") Long id);
int insert(@Param("customer") Customer customer);
int update(@Param("customer") Customer customer);
int delete(@Param("id") Long id);
}
@Param
は引数を後述のXMLのSQLステートメントで参照するための名前を指定します。
CustomerMappler.xml
<?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="com.example.dbunitdemo.domain.mapper.CustomerMapper">
<select id="findAll" resultType="com.example.dbunitdemo.domain.model.Customer">
SELECT id, name, age, address FROM customer
</select>
<select id="get" resultType="com.example.dbunitdemo.domain.model.Customer">
SELECT id, name, age, address FROM customer WHERE id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO customer (name, age, address) VALUES (#{customer.name}, #{customer.age}, #{customer.address})
</insert>
<update id="update">
UPDATE customer SET name = #{customer.name}, age = #{customer.age}, address = #{customer.address} WHERE id = #{customer.id}
</update>
<delete id="delete">
DELETE FROM customer WHERE id = #{id}
</delete>
</mapper>
補足:MapperのXML作成の注意点
-
main/resources
配下にJavaのパッケージ階層と同じディレクトリ階層を作成して(Mapper名).xml
ファイルを作成する。 -
<mapper namespace="...">
にはMapperインタフェースのFQCNを指定する。 -
<insert>
、<select>
、<update>
、<delete>
のid
属性にMapperインタフェースのメソッド名を指定し、コンテンツ部分にSQLを記述してMapperのメソッドとSQLを紐付ける。 -
<select resultType="...">
には検索結果をマッピングするModelクラスのFQCNを指定する。 -
<insert>
で登録時に、自動採番された値を元のModelのプロパティにセットしたい場合useGeneratedKeys="true"
とkeyProperty="id"
を組み合わせて指定する。
AppConfig.java
最後にMapperインタフェースの属するパッケージをコンフィグクラスのアノテーションで指定します。
package com.example.dbunitdemo.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.example.dbunitdemo.domain.mapper") // Mapperの属するパッケージを指定
public class AppConfig {
// ... 省略
}
以上でCustomerMapper
の作成は完了です。
Mapperの作成後のディレクトリ構成は以下のようになっています。
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── dbunitdemo
│ │ ├── DbunitDemoApplication.java
│ │ ├── config
│ │ │ └── AppConfig.java
│ │ └── domain
│ │ ├── mapper
│ │ │ └── CustomerMapper.java
│ │ └── model
│ │ └── Customer.java
│ └── resources
│ ├── application.yml
│ ├── com
│ │ └── example
│ │ └── dbunitdemo
│ │ └── domain
│ │ └── mapper
│ │ └── CustomerMapper.xml
2-3. Mapperを利用するRepositoryクラスの作成
シンプルにDIしたMapper
をRepository
から利用します。
package com.example.dbunitdemo.domain.repository;
import com.example.dbunitdemo.domain.mapper.CustomerMapper;
import com.example.dbunitdemo.domain.model.Customer;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class CustomerRepository {
private final CustomerMapper customerMapper;
public List<Customer> findAll() {
return customerMapper.findAll();
}
public Customer get(Long id) {
return customerMapper.get(id);
}
public int create(Customer customer) {
return customerMapper.insert(customer);
}
public int update(Customer customer) {
return customerMapper.update(customer);
}
public int delete(Long id) {
return customerMapper.delete(id);
}
}
Mapper
をラップしてるだけで意味のないクラスに思えるかもしれませんが、
Service
がインフラやミドルウェアに依存しないようにするために必要なことです。
例えば、Repository
を介さずに直接Service
からMapper
を利用した場合、Service
のロジック内にMyBatis特有の処理(例えばExampleを使ったクエリ構築)や、下手したらDBがMySQLであることを意識した処理なんかが実装されてしまう可能性があります。
Service
はRepository
のトランザクション管理に注力すべきで、インフラやミドルウェアに依存した処理は実装すべきではありません。
そのような部分を隠蔽するためにRepository
を作成します。
3. 動作確認する
ここまで作ったら動作確認したいので、サクッとRepository -> Service -> Controller
まで繋げてみます。
ひとまず、DBの初期データの表示を確認するために findAll
メソッドにだけ実装してみます。
@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;
@Transactional(readOnly = true)
public List<Customer> findAll() {
return customerRepository.findAll();
}
}
@RequiredArgsConstructor
@RestController
public class CustomerController {
private final CustomerService customerService;
@GetMapping("customers")
public List<Customer> findAll() {
return customerService.findAll();
}
}
ここまで作成したらbootRun
でアプリケーションを起動して、ブラウザから http://localhost:8080/customers
にアクセスしてみます。
無事に投入していたデータが表示されました!
次回はDBUnitでテストしてみる
今回はMySQL
の設定、MyBatisのMapper
とそれを利用するRepository
の実装をまとめました。
次回はこれらをDBUnitを使ったテストケースの実装を紹介していきます。