JDBCでH2 Databaseに接続
spring batchのtutorailやってる中で個人ブログを参考にいくつか試していたが、DB周りの記載が曖昧で結局自力で繋げなかったため整理する。 とりあえず、DBとDBと繋ぐためのライブラリが複数あるので一通り試したい。今回は一番飾り気の無いH2 DatabaseとJDBCを試す
- DBの種類
- H2
- MySQL
- SQLite
- javaでDBへ繋ぐためのいい感じのライブラリ(多分)
- JDBCドライバ
- Mybatis
- Jooq
概要
ユーザ名を登録、登録されたユーザ名全件を返す簡単なAPIを2個実装してその中でinsertとselectを試してみる。
やること
- spring initializerでプロジェクト作成
- ドメイン作成
- Repository作成
- Service作成
- API作成
- DB設定
- API叩いてみる
1. spring initializerでプロジェクト作成
spring initializerで書き必要なライブラリ周りを揃えたプロジェクトを作成。
- Spring Web
- lombok
- H2 Database
- MySQL Driver
ディレクトリ構成
.
├── HELP.md
├── build
├── build.gradle
├── gradle
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── springjdbc
│ │ ├── Service
│ │ │ ├── FindUserNameService.java
│ │ │ └── RegisterUserNameService.java
│ │ ├── SpringMybatisApplication.java
│ │ ├── api
│ │ │ ├── FindUserName.java
│ │ │ ├── RegisterUserName.java
│ │ │ └── RegisterUserNameRequest.java
│ │ ├── domain
│ │ │ ├── UserName.java
│ │ │ └── UserNameRepository.java
│ │ └── repository
│ │ └── UserNameRepositoryJdbcImpl.java
│ └── resources
│ ├── application.properties
│ ├── data.sql
│ ├── schema.sql
│ ├── static
│ └── templates
└── test
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
}
tasks.named('test') {
useJUnitPlatform()
}
2. ドメイン作成
ユーザ名を保持するドメインを作成する。なんとなく姓と名を持つrecordクラスにした
UserName.java
コンストラクタはof
メソッドで実装。
package com.example.springjdbc.domain;
public record UserName (String FirstName, String LastName) {
public static UserName of (String firstName, String lastName) {
return new UserName(firstName, lastName);
}
}
3. Repository作成
クリーンアキテクチャに則り(?)Interface切ってレポジトリを実装する。Interfaceはdomain
配下に、実装はrepositoryパッケージ
配下に作成しているものが多かったためそれに習い作成。
UserNameRepository.java
DBに登録されたユーザ名全件取得するfindAll
と渡されたユーザ名をDBへ登録するinsertUserName
を想定。
package com.example.springjdbc.domain;
import java.util.List;
public interface UserNameRepository {
List<UserName> findAll();
int insertUserName(UserName userName);
}
UserNameRepositoryJdbcImpl.java
JdbcTemplate
をAutowired
でインジェクションすれば直ぐにJDBCを使える。
使用方法は大体まんまSQLを書き、必要ならVALUES
で値を渡してやる感じ。注意点としてはinsert,update,deleteもupdateメソッドで実行する。updateメソッドは成功すると影響した件数を返してくれる。
queryForListは結果をテーブル列名をキー、値に検索結果を入れたMapをListにまとめて返してくれる。
ただ変換が面倒なためjdbcTemplate.query(SQL文,new DataClassRowMapper<>(変換先クラス名.class)
を指定しておけばクラスにselect結果をマッピングしListで返してくれる。
List<
MAP<("列名","値")>,
Map<("FIRST_NAME","山田"),("LAST_NAME","太郎")>,
Map<("FIRST_NAME","田中"),("LAST_NAME","太郎")>>
package com.example.springjdbc.repository;
import com.example.springjdbc.domain.UserName;
import com.example.springjdbc.domain.UserNameRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserNameRepositoryJdbcImpl implements UserNameRepository {
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public int insertUserName(UserName userName) {
return jdbcTemplate.update(
"INSERT INTO USER_NAME (FIRST_NAME, LAST_NAME) VALUES(?, ?)"
,userName.FirstName()
,userName.LastName());
}
@Override
public List<UserName> findAll() {
return jdbcTemplate.queryForList(
"SELECT * FROM USER_NAME"
).stream()
// Map<String, Object>で検索結果が返ってくるため、列名をキーに値を取り出してUserNameを作成
.map(record -> UserName.of((String)record.get("FIRST_NAME"),(String)record.get("LAST_NAME")))
.toList();
}
@Override
public List<UserName> findAll() {
return jdbcTemplate.query(
"SELECT * FROM USER_NAME",
new DataClassRowMapper<>(UserName.class)
);
}
}
3. Service作成
FindUserNameService.java
DBへ登録されたユーザ名すべてを返却するユースケース用のサービス。作成したInterface
の方ををインジェクションして使う。
package com.example.springjdbc.service;
import com.example.springjdbc.domain.UserName;
import com.example.springjdbc.domain.UserNameRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class FindUserNameService {
@Autowired
UserNameRepository userNameRepository;
public List<UserName> findAll() {
return userNameRepository.findAll();
}
}
RegisterUserNameService.java
渡されたユーザ名をDBへ登録するユースケース用のサービス。
package com.example.springjdbc.service;
import com.example.springjdbc.domain.UserName;
import com.example.springjdbc.domain.UserNameRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class RegisterUserNameService {
@Autowired
UserNameRepository userNameRepository;
public boolean insert(UserName userName) {
int rowNumber = userNameRepository.insertUserName(userName);
return rowNumber == 1;
}
}
4. API作成
FindUserName.java
登録されているユーザ名全件返すAPI。GET
で/user_name/
を呼び出すと呼ばれる。
package com.example.springjdbc.api;
import com.example.springjdbc.service.FindUserNameService;
import com.example.springjdbc.domain.UserName;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequiredArgsConstructor
public class FindUserName {
private final FindUserNameService service;
@RequestMapping(value = "/user_name/", method = RequestMethod.GET)
public Map invoke(){
List<UserName> userNameList = service.findAll();
Map<String, List<UserName>> result = new HashMap<>();
result.put("user_name", userNameList);
return result;
}
}
RegisterUserName.java
Json形式で姓・名を受取DBへ保存するAPI。
受け取ったJsonは一度RegisterUserNameRequest
クラスへマッピンさせ、ドメインであるUserNameのインスタンを作成する。マッピングの時点でUserNameを作成することもできるが一度全てリクエスト用クラスで受け、変換させる方針にした。どっちでも良くこの辺は宗派の違いらしい。
package com.example.springjdbc.api;
import com.example.springjdbc.domain.UserName;
import com.example.springjdbc.service.RegisterUserNameService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequiredArgsConstructor
public class RegisterUserName {
private final RegisterUserNameService service;
@RequestMapping(value = "/user_name", method = RequestMethod.POST)
public Map invoke(@RequestBody RegisterUserNameRequest request) {
service.insert(UserName.of(request.FirstName(),request.LastName()));
Map<String, String> status = new HashMap<>();
status.put("Status","200");
return status;
}
}
RegisterUserNameRequest.java
いや、ほんとUserName.java
と同じ。
package com.example.springjdbc.api;
public record RegisterUserNameRequest(String FirstName, String LastName) {
}
5. DB設定
DBへの接続、テーブルの作成、初期データ登録がやりたいこと。H2
もしくはHSQL
であれば接続設定無しで依存関係だけ書いておけば勝手に繋いでくれる。
schema.sql
resources
配下にschema-xxみたいな名前でSQL置いておくと勝手にアプリ立ち上げ時にSQLを実行してくれる。この仕組みを使ってテーブルを作成する
CREATE TABLE IF NOT EXISTS USER_NAME
(
FIRST_NAME VARCHAR(64) NOT NULL,
LAST_NAME VARCHAR(64) NOT NULL
);
data.sql
上記でテーブルは作成できたが、初期データが無いと面白くないので登録もしておく。仕組みは同じでdata-xxな名前でSQL書いておけばアプリ起動時に実行してくれるので、初期データを登録する。
INSERT INTO USER_NAME(FIRST_NAME, LAST_NAME) VALUES ('Tanaka', 'Taro');
6. API叩いてみる
登録されたユーザ名全件参照API
% curl -X GET localhost:8080/user_name/ | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 56 0 56 0 0 427 0 --:--:-- --:--:-- --:--:-- 430
{
"user_name": [
{
"FirstName": "Tanaka",
"LastName": "Taro"
}
]
}
ユーザ名登録API
curlの-dオプションに@-
でヒアドキュメントから入力を受けつけてくれる。
% curl -X POST localhost:8080/user_name -H "Content-Type: application/json" -d @- <<EOF | jq .
{
"FirstName":"山田",
"LastName":"太郎"
}
EOF
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 64 0 16 100 48 262 788 --:--:-- --:--:-- --:--:-- 1066
{
"Status": "200"
}
登録されたユーザ名全件参照API
追加したので山田も返ってくる
% curl -X GET localhost:8080/user_name/ | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 99 0 99 0 0 19245 0 --:--:-- --:--:-- --:--:-- 19800
{
"user_name": [
{
"FirstName": "Tanaka",
"LastName": "Taro"
},
{
"FirstName": "山田",
"LastName": "太郎"
}
]
}