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

技術

今回はいよいよリポジトリパターン

リポジトリパターンを学ぶ前段階として、前回Spring Boot & H2DBの開発環境をDocker上に作り、DAOパターンを試してみました。

今回は、いよいよ本丸のリポジトリパターンを試してみます。

せっかくなのでリポジトリパターンを使ったCRUDな機能を持った画面も作ってみたいと思います。

それでは早速やってみましょう!!

プロジェクトを作成

前回作ったdemoプロジェクトをそのまま使ってもいいんですが、今回は新たに別のプロジェクトを作り直して1からやってみることにします。

DBは前回用意したH2DBをそのまま使うことにします。

では、まずはプロジェクト作成から。

Specify Spring Boot version.3.2.2
Specify project language.Java
Input Group Id for your project.biz.systemcraft※任意の値
Input Artifact Id for your project.demo2※任意の値
Specify packaging type.Jar
Specify Java version.17
Search for dependencies.* Spring Web
* Lombok
* Spring Data JPA
* H2 Database
* Thymeleaf

プロジェクトの保存場所は前回と同じ「/usr/local/src」としました。
プロジェクト名は「demo2」です。
この辺りはお好みでどうぞ。

依存関係には前回選択したSpring WebLombokに加えてSpring Data JPAH2 DatabaseThymeleafの3つを追加しています。

生成されたdemo2プロジェクト内のbuild.gradleを確認すると、前回は直接編集して追加していた下記2つの依存関係が最初から追加されていることがわかると思います。

	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'

これでプロジェクトは生成できたわけですが、コーディングを始める前にもう一つだけやっておきたいことがあります。

前回、DAOパターンを試した時にH2DBへの接続情報はsrc/main/resources/META-INF/persistence.xmlに定義しましたが、今回のリポジトリパターンではsrc/main/resources/application.propertiesに定義することになります。

事前準備として最後にapplication.propertiesだけ用意しておきましょう。

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/~/test-db
spring.datasource.username=sa
spring.datasource.password=

これで準備完了です。
それでは早速作っていきましょう!!

Model & Repository を作成

今回のリポジトリパターンでは前回、サンプルで作ったtest-dbusersテーブルを使います。
一度、usersテーブルのCREATE文を確認しておきましょう。

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

このusersテーブルに合わせたレイアウトでUsersモデルクラスを作ります。
このクラスはほとんど前回と同じなのでコピペして、修正すればOKです。
前回同様「model」ディレクトリの下に作成しました。

package biz.systemcraft.demo2.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;

@Entity
@Data
public class Users {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private Integer age;
}

続いていよいよリポジトリです。
こちらはクラスではなくインターフェイスなのでご注意を。
作成場所は「repositories」ディレクトリの下にしました。

package biz.systemcraft.demo2.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import biz.systemcraft.demo2.model.Users;

@Repository
public interface  UsersRepository extends JpaRepository<Users, Integer> {
    
}

たったこれだけでH2DBのusersテーブルにアクセスする最低限のCRUDは実装できてしまいました。

ちなみにフォルダ構成はこんな感じです。

動作確認

ここで一度データが取得できるかどうか、簡単な画面を作って試してみます。

「controllers」ディレクトリを作ってIndexController.javaを作成します。

package biz.systemcraft.demo2.controllers;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import biz.systemcraft.demo2.model.Users;
import biz.systemcraft.demo2.repositories.UsersRepository;

@Controller
public class IndexController {
    @Autowired
    private UsersRepository usersRepository;

    @GetMapping("/")
    public String getIndex(Model model) {
        List<Users> users = usersRepository.findAll();
        model.addAttribute("users", users);
        return "index";
    }
}

UsersRepositoryをprivateフィールドとして宣言していますが@Autowiredアノテーションを付けることで、UsersRepositoryのインスタンス「usersRepository」が DI されるようにしています。

そしてusersRepositoryはgetIndexメソッド内でfildAll()メソッドを呼び出してusersテーブルからデータを取得しています。

ところで、このUsersRepository.findAll()実装した覚えがありません
それどころかUsersRepository自体、インターフェイスを宣言しただけで、クラスの実装はどこにもありません

実はUsersRepositoryはJpaRepositoryの派生インターフェイスとして宣言したために、実態となるクラスもfindAllやその他の一般的な処理についても自動的に実装されていて、ほとんどコードを書くことなしに使えてしまうのです。

続いて画面のhtmlを作ります。
こちらは「resources/templates」ディレクトリの下にindex.htmlとして作成します。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8"/>
   <title>Demo2</title>
</head>
<body>
<main>
    <h1>ユーザーリスト</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>名前</th>
                <th>年齢</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="user:${users}" th:object="${user}">
                <td th:text="*{id}"></td>
                <td th:text="*{name}"></td>
                <td th:text="*{age}"></td>
            </tr>
        </tbody>
    </table>
</main>
</body>

Thymeleafを使って、IndexControllerで取得したusersテーブルの情報がhtml上に動的に出力されるように作っています。

Thymeleafについては以前の記事でも扱っているので、よければこちらも見てください。

そんなわけで、実装できたら実行してみましょう。
前回と同じようにDebugビューから「Run and Debug」ボタンで実行します。

こんな感じの画面が表示されたら成功です。

CRUD機能を実装

先ほど、「UsersRepositoryクラスにはfindAllやその他の一般的な処理についても自動的に実装されている」と説明しました。

「その他の一般的な処理」の中にはいわゆるCRUDの機能も含まれています。
ということで最後に、登録/編集/削除の機能を持つ画面を実装してみましょう。

まずは先ほど作ったindex.htmlを編集します。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8"/>
   <title>Demo2</title>
   <link href="/css/style.css" rel="stylesheet" />
</head>
<body>
<main>
    <h1>ユーザーリスト</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>名前</th>
                <th>年齢</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="user:${users}" th:object="${user}">
                <td th:text="*{id}"></td>
                <td th:text="*{name}"></td>
                <td th:text="*{age}"></td>
                <td style="width: 120px;"><a th:href="@{/edit/{id}(id=*{id})}"><button class="btn-edit">編集</button></a></td>
            </tr>
        </tbody>
    </table>
    <footer>
        <a href="/create"><button id="btnNew">新規登録</button></a>
    </footer>
</main>
</body>

次にusersデータを登録/更新/削除する画面をusers.htmlとして作成します。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8"/>
   <title>Demo2</title>
   <link href="/css/style.css" rel="stylesheet" />
</head>
<body>
<main>
    <h1 th:text="${user != null} ? '更新/削除' : '新規登録'"></h1>
    <form method="post">
        <th:block th:if="${user != null}">
            <div class="input-group">
                <label><span>ID</span></label>
                <input type="text" name="id" th:value="${user.id}" readonly>
            </div>
        </th:block>
        <div class="input-group">
            <label><span>名前</span></label>
            <input type="text" name="name" th:value="${user != null} ? ${user.name} : ''">
        </div>
        <div class="input-group">
            <label><span>年齢</span></label>
            <input type="number" name="age" th:value="${user != null} ? ${user.age} : ''">
        </div>
        <footer>
            <th:block th:if="${user != null}">
                <button style="width: 150px;" formaction="/update">更新</button>
                <button style="width: 150px;" formaction="/delete" class="delete-button">削除</button>
            </th:block>
            <th:block th:if="${user == null}">
                <button formaction="/create">登録</button>
            </th:block>
        </footer>
    </form>
</main>
</body>

今回は画面デザインも多少はいい感じになるようにcssも用意してみました。
cssやjsファイルはresources/staticの配下に作成します。

@media screen and (min-width: 1201px) {
    main {
        width: 1200px;
        margin: 20px auto;
    }
}

h1 {
    text-align: center;
}
table {
    width: 100%;
}
table td {
    padding: 5px;
}
thead tr {
    background-color: gainsboro;
}
tbody tr:nth-child(odd) {
    background-color: aliceblue;
}
footer {
    margin: 20px;
    text-align: center;
}
button {
    width: 100%;
    max-width: 300px;
    appearance: none;
    border: 0;
    border-radius: 5px;
    background: #4676D7;
    color: #fff;
    padding: 4px 8px;
    font-size: 16px;
}
button.delete-button {
    background: #d95e4e;
}
button:hover {
    background: #1d49aa;
}
button.delete-button:hover {
    background: #bb4938;
}
button:focus {
    outline: none;
    box-shadow: 0 0 0 4px #cbd6ee;
}
button:focus {
    box-shadow: 0 0 0 4px #cbd6ee;
}
button.delete-button:focus {
    outline: none;
    box-shadow: 0 0 0 4px #eecbcb;
}
button:disabled {
    color: #d2d5db;
    background: #6c7589;
    cursor: not-allowed;
}

label {
    font-weight: bold;
}

.input-group {
    display: flex;
    margin: 20px 100px;
}

.input-group label {
    width: 50%;
    margin-block: auto;
    text-align: right;
}
.input-group label span {
    margin-inline: 50px;
}

input[type=text],
input[type=number] {
    padding: 8px;
    border-radius: 6px;
}

input[type=text] {
    width: 200px;
}

input[type=number] {
    width: 80px;
}

最後にUsersController.javaを作ります。
ここがUsersRepositoryのCRUD機能を使う部分になります。

package biz.systemcraft.demo2.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import biz.systemcraft.demo2.model.Users;
import biz.systemcraft.demo2.repositories.UsersRepository;


@Controller
public class UsersController {
    @Autowired
    private UsersRepository usersRepository;

    @GetMapping("/create")
    public String getCreate(Model model) {
        model.addAttribute("user", null);
        return "users";
    }

    @GetMapping("/edit/{id}")
    public String getEdit(@PathVariable("id") int id, Model model) {
        Users user = usersRepository.findById(id).get();
        model.addAttribute("user", user);
        return "users";
    }

    @PostMapping("/create")
    public String postCreate(@RequestParam("name") String name, @RequestParam("age") Integer age, Model model) {
        Users user = new Users();
        user.setName(name);
        user.setAge(age);
        usersRepository.save(user);
        return "redirect:/";
    }

    @PostMapping("/update")
    public String postUpdate(@RequestParam("id") Integer id, @RequestParam("name") String name, @RequestParam("age") Integer age, Model model) {
        Users user = new Users();
        user.setId(id);
        user.setName(name);
        user.setAge(age);
        usersRepository.save(user);
        return "redirect:/";
    }

    @PostMapping("/delete")
    public String postDelete(@RequestParam("id") Integer id, Model model) {
        usersRepository.deleteById(id); 
        return "redirect:/";
    }}

コードの細かい説明は省略しますが、UsersRepository.findById()Readして、UsersRepository.save()Create/Updateし、UsersRepository.deleteById()Deleteしていることが確認できると思います。

「findById」も「save」も「deleteById」も、いずれも「findAll」同様、実装した覚えのないものだと思いますが、ちゃんと動きます。

これでusersテーブルにデータを登録して、編集/削除する機能が実装できました。
入力チェックなど、細かい部分は省略していますが、とりあえず機能としては成立しているはずです。

実行してみると、こんな感じで画面が表示されて新規登録から編集、削除ができますね。

ちなみに最終的なフォルダ構成はこんな感じになりました。

まとめ

そんなわけで前回と今回の2回に渡ってH2DBとリポジトリパターンを触ってみました。

Springでリポジトリを扱うのはとても簡単でしたね。

もっとも、今回のように1テーブルだけのCRUDで終わるシステムは稀です。
多くの場合、もっと多くのテーブルを対象にしたり、複雑なリレーションが組まれていたりするものです。

そんな時にはリポジトリに自前で専用のメソッドを準備することで対応できたりします。
例えば、下記は20歳以上のusersを抽出するメソッド定義を追加したものです。

package biz.systemcraft.demo2.repositories;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import biz.systemcraft.demo2.model.Users;

@Repository
public interface  UsersRepository extends JpaRepository<Users, Integer> {
    @Query("SELECT u FROM Users u WHERE u.age >= 20")
    List<Users> findAdults();
}

こんな感じで必要に応じて機能を追加していくことができます。

もちろん、今回紹介したコードはJpaRepositoryのほんの触り部分です。
もっと多くの機能があるはずなので、本格的に研究してみてもいいかもしれませんが、今回はここまでにしたいと思います。

コメント

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