spring batchのtutorailやってる中で個人ブログを参考にいくつか試していたが、DB周りの記載が曖昧で結局自力で繋げなかったため整理する。 とりあえず、DBとDBと繋ぐためのライブラリが複数あるので一通り試したい。今回は一番飾り気の無いH2 DatabaseとJDBCを試す

  • DBの種類
    • H2
    • MySQL
    • SQLite
  • javaでDBへ繋ぐためのいい感じのライブラリ(多分)
    • JDBCドライバ
    • Mybatis
    • Jooq

概要

ユーザ名を登録、登録されたユーザ名全件を返す簡単なAPIを2個実装してその中でinsertとselectを試してみる。

やること

  1. spring initializerでプロジェクト作成
  2. ドメイン作成
  3. Repository作成
  4. Service作成
  5. API作成
  6. DB設定
  7. 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

JdbcTemplateAutowiredでインジェクションすれば直ぐに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": "太郎"
    }
  ]
}

参考