【Java】「Spring Boot」&「H2DB」でリポジトリパターンを学ぶ①

技術

H2Databaseを試してみる

最近、まだ1年目の会社の後輩にH2Databaseの環境構築がうまくいかないと、相談を受けました。
JavaのSpring BootでH2DBを使いたいらしいです。

残念ながら相談を受けた時、コードを見せてもらえたり、調べたりできる環境にはなかったので、あまりいいアドバイスはできなかったと思います。

ただ、私自身はH2DBを触ったことがなかったですが、何か特別難しいようなこともないだろうと考えていました。

それより、相談してくれた後輩と話している中で、H2Databaseがどうかということよりも、リポジトリパターンをよくわかってないのかな、という印象を受けました。

そんなわけで、今回はSpring Boot&H2DBの環境でリポジトリパターンを試してみようと思います。

そもそもリポジトリパターンって?

リポジトリパターンとよく似たものにDAOパターンというものがあります。

どちらもデータアクセス部分をビジネスロジックから分離し、隠蔽するものですが微妙に目的が違うものです。

リポジトリパターンはデータ操作に関する処理を抽象化したレイヤー(リポジトリ)に任せることで、ビジネスロジックとデータアクセスを分離します。
リポジトリを通してデータにアクセスするので、リポジトリの裏でデータがどのように保存されているかを意識する必要がなくなります。

DAOパターンはデータアクセスのメカニズムを隠蔽して、ビジネスロジックとデータアクセスを分離するものです。
テーブルに対応するModelを実装して、いわゆるORマッピング等によってDB上のデータとコード上のModelを対応付けます。
Javaの場合、JPAMyBatisといったライブラリがDAOパターンの代表的な実装になります。

説明するより、やってみるのが早そうです。

Spring Boot & H2DB の開発環境

そんなわけでまずは環境づくりから。
いつも通りDocker(version 20.10.24)を使います。

H2DBのインストール

FROM debian:12

WORKDIR /opt

# OpenJDK 17 のインストール
RUN apt update && apt install -y openjdk-17-jdk

# H2のバージョン
ARG h2db_version="2.2.224"
ARG release="2023-09-17"
# Database名、ユーザー、パスワード定義
ARG db_name="test-db"
ARG user="sa"
ARG password=""

# # H2のインストールに必要な wget、unzip をインストール
RUN apt install -y wget unzip

# H2をインストール
RUN wget https://github.com/h2database/h2database/releases/download/version-${h2db_version}/h2-${release}.zip && \
    unzip h2-${release}.zip && \
    rm h2-${release}.zip
# H2 を開始できるように準備
RUN echo webAllowOthers=true > ~/.h2.server.properties && \
    chmod +x h2/bin/h2.sh
# データベースを作成する
RUN java -cp /opt/h2/bin/h2-${h2db_version}.jar org.h2.tools.Shell -url jdbc:h2:~/${db_name} -driver org.h2.Driver -user "${user}" -password "${password}" -sql ""

# データベースを起動する
ENTRYPOINT [ "/bin/sh", "-c", "/opt/h2/bin/h2.sh" ]

H2DatabaseはJavaVM上で動くのでOpenJDKがインストールされている環境をベースイメージにしました。

build & run は下記のコマンドで。

docker build -t h2db .
docker run -itd --name h2db-dev -p 8082:8082 h2db

このDockerfileから作られたコンテナには「test-db」という名前で空のデータベースが作られています。
ユーザー名は「sa」、パスワードは空です。

DB名とユーザーを変更したければ、下記のように Dockerfile のbuild時にパラメータで指定します。

docker build -t h2-testdb --build-arg db_name=develop-db --build-arg user=user --build-arg password=pass .

コンテナが起動したらブラウザからH2のWebコンソール(http://localhost:8082)にアクセスできるはずです。
Webコンソールで下記の通りに入力すれば、test-dbに接続できます。

SpringBootの開発環境

続いてSpringBootの開発環境ですが、今回は先ほど作ったH2DBの動いているコンテナをそのまま利用することにしました。

OpenJDKがインストール済みなのでちょうどいいです。

まず、VSCodeのRemote Development拡張でコンテナにアクセスします。

Visual Studio Code Remote Development
Visual Studio Code Remote Development

コンテナにアクセスできたら、早速SpringBootの開発環境を作っていきましょう。

SpringBootの開発をVSCodeでやるならVSCode拡張のSpring Boot Extension Packを使うのが便利です。
以前も使いましたが今回も使うことにします。

さらに、Extension Pack for JavaLombok Annotations Support for VS Code も入れておきます。
この辺りも前回と一緒です。

これらのVSCode拡張が入ったら準備OK。

「Ctrl + Shift + P」でコマンドパレットを開いてSpring Initializr:Create a Gradle Project...を選択し、下記の通り入力します。

Specify Spring Boot version.3.2.0
Specify project language.Java
Input Group Id for your project.biz.systemcraft※任意の値
Input Artifact Id for your project.demo※任意の値
Specify packaging type.Jar
Specify Java version.17
Search for dependencies.* Spring Web
* Lombok

最後にプロジェクトの保存場所(今回は「/usr/local/src」にしました)を入力したら完了。
/usr/local/srcの配下にdemoディレクトリが出来上がっていればSpringBootのプロジェクトは出来上がりです。

Debugで実行して確認しておきましょう。

「/usr/local/src/demo」ディレクトリをVSCodeを開きなおしたら、DebugビューからRun and Debugをクリックします。

コマンドパレットに下記のように出力されたら、起動は成功しています。

サンプルデータの準備

最後にリポジトリパターンを試してみるためのテーブルをDBに追加しておきます。

H2DBのWebコンソールにアクセスして、ログインしたら、SQLが実行できるので、下記のSQLを実行しておきます。

CREATE TABLE IF NOT EXISTS users (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    age INT
);

INSERT INTO users (name, age) VALUES ('Thomas', 20);
INSERT INTO users (name, age) VALUES ('Percy', 35);
INSERT INTO users (name, age) VALUES ('James', 18);

正常にテーブルができてサンプルデータが登録できたら準備完了です。

まずはH2DBに接続&DAOパターンを試す

今回はJPAを使ったリポジトリパターンをやってみたいのですが、何はともあれ、その前にまずH2DBに接続できなければ話になりません。

やってみましょう。

JPAなしにH2DBへ接続

build.gradleを編集して、dependenciesの中にruntimeOnly 'com.h2database:h2'という行を追加します。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.0'
	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'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2' // ※ ←追加
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

次にサンプルとして用意しておいたusersテーブルに対応するModelを作ります。
このModelクラスは「model」ディレクトリの下に作成することにします。

package biz.systemcraft.demo.model;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Users {
    private Integer id;
    private String name;
    private Integer age;
}

続けて、DemoApplication.javaを編集します。

package biz.systemcraft.demo;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import biz.systemcraft.demo.model.Users;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {

		List<Users> users = new ArrayList<>();
		try (Connection connection = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test-db", "sa", "");
			PreparedStatement ps = connection.prepareStatement("SELECT * FROM users")) {
			try (ResultSet rs = ps.executeQuery()) {
				while (rs.next()) {
					users.add(Users.builder()
						.id(rs.getInt("id"))
						.name(rs.getString("name"))
						.age(rs.getInt("age"))
						.build()
					);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		for (Users user : users) {
			System.out.println(user.getId().toString()
							 + ":" + user.getName() 
							+ "[" + user.getAge().toString() + "]");
		}
		
		SpringApplication.run(DemoApplication.class, args);
	}

}

コネクションを生成して、SQLを組み立てて、取得したデータをオブジェクトにマッピングしているという、よくある実装ですね。

これを実行したらデータが取得できるはずです。

DAOパターンに改造

次にJPAを使ったDAOパターンを試してみます。

build.gradleでJPAを依存関係に追加しましょう。
dependenciesにimplementation 'org.springframework.boot:spring-boot-starter-data-jpa'を追加するだけです。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.0'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'biz.systemcraft'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // ※ ←追加
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

DAOパターンではデータアクセスする機能をDAOクラスに実装します。
今回、DAOクラスは「dao」ディレクトリの下に作成することにしました。

package biz.systemcraft.demo.dao;

import java.util.List;

import biz.systemcraft.demo.model.Users;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.Query;

public class UsersDao {
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("test-db");

    public List<Users> selectAll() {
        EntityManager em = emf.createEntityManager();
        try {
            Query query = em.createNativeQuery("SELECT * FROM users", Users.class);
            return (List<Users>)query.getResultList();
        } finally {
            em.close();
        }
    }
}

次に先ほど「model」ディレクトリに作ったUsersクラスを修正します。

package biz.systemcraft.demo.model;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Data;

@Data
// @Builder
@Entity
public class Users {
    @Id
    private Integer id;
    private String name;
    private Integer age;
}

@Builderアノテーションをコメントアウトして、代わりに@Entityというアノテーションを追加しています。
@EntityアノテーションでORマッピングできるクラスであることを示しているわけです。

続けてDemoApplication.javaを修正します。
先ほどの実装を捨てて、作成したUsersDaoを利用するように下記の通りに書き換えましょう。

package biz.systemcraft.demo;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import biz.systemcraft.demo.dao.UsersDao;
import biz.systemcraft.demo.model.Users;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {

		UsersDao dao = new UsersDao();
		List<Users> users = dao.selectAll();

		for (Users user : users) {
			System.out.println(user.getId().toString()
							 + ":" + user.getName() 
							+ "[" + user.getAge().toString() + "]");
		}
		
		SpringApplication.run(DemoApplication.class, args);
	}

}

「コネクションの作成」と「取得データのオブジェクトへのマッピング」がコードの中から消えて、随分すっきりしました。

データアクセスとORマッピングをJPAが代わりにやってくれているわけです。

ではDBへの接続情報はどこで指定するのかというと、demo/src/main/resources/META-INF/persistence.xmlというファイルに定義することになります。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="test-db">
        <description>A demo persisting to a DB with vanilla JPA</description>

        <class>biz.systemcraft.demo.model.Users</class>

        <properties>
            <!-- Database connection details -->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test-db" />
            <property name="jakarta.persistence.jdbc.user" value="sa" />
            <property name="jakarta.persistence.jdbc.password" value="" />
        </properties>

    </persistence-unit>
</persistence>

これで準備OK!!
あとは先ほどと同じように実行すれば、同じようにちゃんとデータが取れることが確認できると思います。

次はいよいよリポジトリパターンですが・・・

ここまでの内容が思っていた以上に長くなってしまったので、今回はいったんここまでにしようと思います。

本丸のリポジトリパターンの実装は次回の記事で紹介することにします。
ぜひ楽しみにお待ち下さい。

Javaの開発に人手が入用ならシステムクラフトがお手伝いできるかもしれませんよ。
お問い合わせお待ちしています。

コメント

タイトルとURLをコピーしました