Chapter 05

Mybatisを使ったデータの保存

あしたば
あしたば
2021.03.23に更新

この章で学ぶこと

二重送信のケースや、不正な値のバリデーションと行った対策を取ることができました。であれば、これでDBへデータを保存することができるようになります。

この章では、DBの操作の基本中の基本、CRUDのうちCreate,Readを学びます。
その実現のために、比較的導入コストの低いH2 Databaseを用い、DB操作のサポートとしてMybatisを利用します。

この時点で何をするかイメージが付いた方はこの章を読む必要はありません。

  • セットアップ
  • DDLの作成
  • データの加工
  • データの保存

注意

ここで利用するH2 Databaseはテスト、開発用途の軽量なDBです。一般的に本番環境での利用は推奨されません。
本番利用なら、MySQL、MariaDB、PostgreSql、お金持ちの企業ならばOracleやAuroraなどの選択肢がよいです。

セットアップ

さて、MybatisとH2DBをセットアップしていきましょう。
現段階ではどちらもpom.xmlに記載がないので、これらを利用することはできません。

下記3つをpom.xmlのdependenciesに追加しましょう。

<!-- DB -->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.4</version>
</dependency>
<dependency>
	<groupId>org.mybatis.scripting</groupId>
	<artifactId>mybatis-thymeleaf</artifactId>
	<version>1.0.2</version>
</dependency>
<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<version>1.4.200</version>
	<!--<scope>test</scope> 本来テストスコープだけで使うべき-->
</dependency>

mybatis-spring-boot-starterはSpringBoot向けに設定されたMybatisです。
mybatis-thymeleafは少しマイナーなのですが、Mybatis特有のXMLによる設定を回避できる追加ライブラリです。個人的なお気に入りです。
h2は今回主役のデータベースです。

コンフィグ設定

mybatis-spring-boot-starterのオートコンフィグレーション機能によって、application.propertiesにデータベースの設定を記載できます。
今回はH2ですが、Oracleが対象でも書き方そのものは似たようにできます。

さて、application.propertiesを書き換えましょう、、と言う前に、私はproperties形式よりyml形式のほうが好きですので、Renameしてしまいます。

では、下記のように設定しましょう。

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:develop;MODE=Oracle;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    hikari:
      pool-name: ConnectionPool
      leakDetectionThreshold: 5000
      maximum-pool-size: 20
      minimum-idle: 10
      username: sa
      password:
      driver-class-name: org.h2.Driver
    schema: classpath:h2/schema.sql
    initialization-mode: embedded # (DB初期化用SQL↑実行を)常に行う.EMBEDDED=h2dbのとき、の意味. ALWAYSは無条件

datasource.urlがDBへの接続設定部分です。
jdbc:h2:memはH2のインメモリDBモード利用であることを示します。
jdbc:h2:fileとすると、ファイルDBに保存することができ、簡単な永続化が可能です。
(ちなみに、ファイルH2DBを読み込むときに、書き込んだH2DBドライバと別バージョンのドライバで読み込んだらファイルが破損しますので注意が必要です)

他に、MODE=OracleはOracleっぽい仕様を取り込んでくれます。あくまでっぽいだけなので、完全に模倣はできません。たとえば、MERGE等は構文から違いますので互換性はないです。

H2DBに関する細かな設定等はこれを参照してみてください。

DDLの作成

DDL(Data Definition Language)とは、データベースの作成や、保存するデータの形式など、データの定義そのものを定義するSQLです。

残念ながら、本稿でSQLの基礎をカバーするのは目的から外れてしまいます。
簡単なものであれば、ドットインストールが導入には良いと思います。
個人的なお気に入りはsqlzooです。ドットインストールでSQLになれたらSQLクイズに挑んでみてください。

application.ymlに下記の設定を行いました。

    schema: classpath:h2/schema.sql
    initialization-mode: embedded

spring.datasource.schemaは、DDLの実行ファイルパスを設定しています。
spring.datasource.initialization-modeは初期化タイミングを指しています。

今回の設定では、毎回SpringBootを立ち上げるたびにデータの初期化が行われます。

本来はビジネスとしてサービスを提供するにあたって、要件定義のステップをしっかり踏んでからER図を書き起こして詳細を詰めていきます。
が、今回はそういう趣旨でもないので、前回の内容からデータを類推しましょう。

前回登場したデータは次のとおりです。

  • 名前。Nullでもよい。最大20文字まで
  • メールアドレス。Nullでもよい。最大100文字まで
  • 投稿内容。Nullを許容しない。最大400文字まで

これらは掲示板への投稿内容ですから、下記のようなDDLとして、h2/schema.sqlを作ります。

CREATE TABLE IF NOT EXISTS USER_COMMENT (
    ID NUMBER(10) AUTO_INCREMENT, -- 主キーとしてふさわしいものがないのでIDを採番
    NAME VARCHAR2(20),
    MAILADDRESS VARCHAR2(100),
    TEXT VARCHAR2(400) NOT NULL,  -- NULLを許容しない
    CONSTRAINT ID_PKC PRIMARY KEY(ID) -- IDをプライマリキーとする。(プライマリキーの名前をID_PKCとする)
);

ちなみにH2のデータ型は幅広く、様々なDBを方言が使えます。詳しくはこちら

CREATE TABLEは文字通り、テーブルの作成です。
IF NOT EXISTS USER_COMMENTは、EXISTS=存在していたら、の否定ですから、もし存在していなければ、となります。
組み合わさって、もしUSER_COMMENTテーブルが存在していなければ作成する、の意味です。

AUTO_INCREMENTは自動採番、つまり1ずつIDを進めていく、という指定です。
CONSTRAINT ID_PKC PRIMARY KEY(ID)はIDカラムに主キー制約を与えています。

このDDLは、前述の通りアプリケーションが実行されるたびに読み出されます。
この段階で一度アプリケーションを実行してみましょう。

o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:develop'

うまく起動できていれば、このような記述が出ていると思います。
これは後ほど紹介しますが、h2の開発サポート機能の一つが有効化されていることを示しています。

データの加工

application層で受け取ったデータ(Formオブジェクト)をそのままDBまで流すこともできますが、ほとんどのアプリケーションではそのようにはなりません。
なにかしらデータを加工したり、計算するロジック、ビジネスロジックが挟まります。

例えば、今回の元ネタの掲示板であれば、

  • 特定の日に、名前を!omikujiにするとおみくじが引け、結果が名前になる
  • メールアドレス欄にsageとかageとか書くとスレッドの表示順が変わる
  • 直近で同じIPから同じ内容の投稿が一定数あったら書き込み制限する

なんていうビジネスロジックがあったりします。
ひとつの機能は1つのビジネスロジックで存在していることもあれば、様々なビジネスロジックを組み合わせで機能が実現されていることもあるでしょう。

今回のアプリケーションでは非常に単調な処理しか無いため、「ビジネスロジックを組み合わせる」ような体験は提供できませんが、簡単な流れを体験してみましょう。

ドメイン

まずは、ユーザから提供されたデータであるCommentFormのデータをドメイン層に持っていきましょう。
このように、ドメインパッケージを作成します。

modelはドメインオブジェクトを、typeはバリューオブジェクトを指します。

バリューオブジェクト

バリュー(値)オブジェクトについて(も)詳しく述べている文献に、現場で役立つシステム設計の原則があります。

僭越ながら引用すると、

専用の型は業務的に不適切な値が混入するバグを防ぎます。業務ルールの変更が必要になったときにも、クラス名やインタフェース名を手がかりに変更の対象箇所を特定しやすくなります。変更の影響範囲もその型のクラス内に閉じ込めやすくなります。

値の種類ごとに専用の型を用意するとコードが安定し、コードの意図が明確になります。

現場で役立つシステム設計の原則 P32

とあります。
今回の例では、名前にはName型を、MailAddressにはMailAddress型を、コメントにはComment型を用意すべきでしょう。

void sendMail(Sring mailAddress)
のような関数よりも、
void sendMail(MailAddress mailAddress)
のほうが安全であるとがわかるでしょうか。

StringといったPOJOは扱いやすく、早く作ることができる一方で、可読性や保守性の観点で問題になりやすいです。
なぜなら、その安全性の担保はコードのバリデーションと変数名から読み取るしかなく、ヒューマンエラーが常に介在するからです。
独自の値型さえ使っていれば、少なくとも意図しない値を流し込まれることはなくなるはずです。
(コンパイラでチェックされるため。)

静的型付けの特徴を最大限に活かすことができるので、短期的な開発速度が落ちたとしても、採用すべき開発手法の一つだと思います。

では、それぞれ作成してみましょう。

内容はそれぞれ次のとおりです。

package chalkboard.me.bulletinboard.domain.type;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Comment {
  private final String value;

  public static Comment from(String comment) {
    return new Comment(comment);
  }

  @Override
  public String toString() {
    return value;
  }
}
package chalkboard.me.bulletinboard.domain.type;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

import java.util.Objects;

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class MailAddress {
  private final String value;

  public static MailAddress from(String mailAddress) {
    return new MailAddress(mailAddress);
  }

  @Override
  public String toString() {
    if(Objects.isNull(value)) {
      return "";
    }
    return value;
  }
}
package chalkboard.me.bulletinboard.domain.type;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

import java.util.Objects;

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Name {
  private final String value;

  public static Name from(String name) {
    return new Name(name);
  }

  public boolean equals(String name) {
    return value.equals(name);
  }

  @Override
  public String toString() {
    if(Objects.isNull(this.value)) {
      return "名無しさん";
    }
      return value;
  }
}

少し特徴的なコードですので、解説を入れます。
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)private finalなフィールドについて、プライベートなコンストラクタを作成してくれます。
プライベートなコンストラクタ、ということはつまり、自分のクラスの中でしかnewできないということです。

それはつまり、このクラスがもつfromメソッドしか、このクラスのインスタンス化の方法を知らないということになります。このようなメソッドを ファクトリメソッド といいます。(GoFのものとは関係ありません。Effective Java3版 項目1に記載があります。)

ファクトリメソッドはnewをそのまま使うよりも安全で、柔軟なコーディングを可能にします。たとえば、newには不可能な「値を検査してからインスタンスを作成する」という挙動を取ることができます。
また、そのインスタンスの「生成方法」に名前をつけることができます。

さて、少し前置きが長くなりましたが、バリューオブジェクトを作成することができましたのでドメインオブジェクトを作成してみましょう。

ドメインオブジェクト

ドメインオブジェクトは、ドメイン(問題領域)を表すオブジェクトです。
問題領域とは、そのアプリケーションやWebサービスが提供する活動そのものを指しています。つまりは、そのアプリケーションを構成する機能それぞれを組み立てているロジックを表現するものです。

ドメインオブジェクトは、業務データ、すなわちバリューオブジェクトを使ってデータの判断、加工を行い機能を表現します。

試しに、ユーザが掲示板に書き込んだデータを表す、UserCommentモデルを作ってみましょう。

内容は下記のようになります。

package chalkboard.me.bulletinboard.domain.model;

import chalkboard.me.bulletinboard.domain.type.Comment;
import chalkboard.me.bulletinboard.domain.type.MailAddress;
import chalkboard.me.bulletinboard.domain.type.Name;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class UserComment {
  private final Name name;
  private final MailAddress mailAddress;
  private final Comment comment;

  public static UserComment from(String name, String mailAddress, String comment) {
    return new UserComment(
        Name.from(name),
        MailAddress.from(mailAddress),
        Comment.from(comment)
    );
  }
}

このままでは、ドメインオブジェクトをドメインオブジェクトたらしめるロジックが存在していません。
それでは面白くないので、一つ機能を追加してみましょう。

特定の名前のときに、おみくじを引く事のできる機能を実装してみましょう。
名前の取得処理に強制的に差し込ませたいのでgetNameとして実装します。

  /**
   * 名前が[!omikuji]ならばおみくじの結果を返す.
   * そうでないならばNameをそのまま返す.
   * @return なまえ
   */
  public Name getName() {
    if(!name.equals("!omikuji")) return name;

    int random = new Random().nextInt(3);

    switch (random) {
      case 0:
        return Name.from("大吉");
      case 1:
        return Name.from("中吉");
      default:
        return Name.from("小吉");
    }
  }

他にも、同一IPや直近書き込まれた投稿などを引数にとって、不正な連続投稿を判定するようなロジックがあってもいいかもしれません。
禁則されているURLのリンクを防いだり、暴言を防いだり、意味のない投稿を防いだり、このクラスでバリューオブジェクトを使って実現できることはたくさんあります。

ユースケースの表現

これでドメイン層の下地が整いました。
ドメインオブジェクトを使って、サービスのユースケースを表現するアプリケーション層を追加してみましょう。

UserCommentUseCaseクラスを追加します。

package chalkboard.me.bulletinboard.application.usecase;

import chalkboard.me.bulletinboard.application.form.CommentForm;
import chalkboard.me.bulletinboard.domain.model.UserComment;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserCommentUseCase {

  /**
   * ユーザの書き込みをDBに反映し、表示するデータをプレゼンテーション層に渡す
   * @param commentForm ユーザの入力データ
   * @return 表示するデータ
   */
  public void write(CommentForm commentForm) {
    // フォームオブジェクトからドメインオブジェクトへ変換
    UserComment userComment = UserComment.from(
        commentForm.getName(),
        commentForm.getMailAddress(),
        commentForm.getComment()
    );

    // 例えばここで、直近の投稿の一覧を取得し、今回と同じ内容の投稿がないかチェックする
  }
}

今回はユーザに投稿したデータをDBをに書き込むというユースケースをここで表現していきます。
ビジネスロジックとしてやるべきことがあれば、ここに書き込んでいく事になります。
例えば、前例にあるようなバリデーション処理です。

値一つを見て入れてはいけない値を判定するバリデーションもあれば、複数のデータを見ないとわからないバリデーションもあります。
直近投稿されたものと同じ投稿は受け入れない、というユースケースは複数のデータをみないとわかりません。

データの保存

今回作成したユースケースの最後では、DBへの書き込み処理を行います。
それを担う機能を作成してみましょう。

DBへアクセスする役割を持つクラスをRepositoryと呼びます。
これは永続化の抽象と説明されています。難しい言葉ですが、簡単に言うと
「先で何やってるかは知らないけど、データを保存してくれている(永続化してくれる)もの」であるということです。

ファイルに書いていようが、メモリに書いていようが、ネットワーク越しにDBサーバに書き込んでいようが、それはデータを保存するという目的を果たしています。
Repositoryを使う側は、単に保存するという目的だけを気にしておけばよく、何に書き込むかを気にするのは設計としておかしいから、永続化のために必要なインタフェースだけあればいいよね、という考えの上に成り立っています。

DTO

そろそろドメインオブジェクトのデータをDBに保存してみたいですよね。後一歩です。
実は、独自クラスだらけのドメインオブジェクトはそのままではDBに渡すことができません。
どのようにデータを扱えばいいか、DBが知らないからです。

これを解決するには大きく2つの手段が取られます。

  • DTOを使って単純なオブジェクトにする
  • TypeHandler を使って独自クラス(バリューオブジェクトやドメインオブジェクト)をどう扱うかをDBに教える

今回はDTO(Data Transfer Object)を利用します。
DTOとは名前のごとく、データ変換用のオブジェクトです。

これを用意しておくことで、アプリケーション側の要件が変更されても、DTOを使っている箇所で影響を吸収することができるようになり、要件変更への耐性が高まります。
多少冗長に感じるかもしれませんが、設けておくと半年後くらいに楽ができるかもしれません。

今回のDTOはデータベースとの境界で値の変換を行いますので、基本的にDBに入れたい内容をシンプルに持つことになります。
DTOパッケージを作成し、UserCommentDtoクラスを作成しましょう。

内容はこのようになります。

package chalkboard.me.bulletinboard.application.dto;

import chalkboard.me.bulletinboard.domain.model.UserComment;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class UserCommentDto {
  private final String name;
  private final String mailAddress;
  private final String comment;

  public static UserCommentDto from(
      UserComment userComment) {
    return new UserCommentDto(
        userComment.getName().toString(),
        userComment.getMailAddress().toString(),
        userComment.getComment().toString()
    );
  }
}

ドメインオブジェクトをDBに保存することはおすすめしません。少なからず、DBでどう使われるかをドメインオブジェクトが気にかけないといけないのは間違っています。(ドメインモデル貧血症に陥りやすくなります)

具体的に言うと、永続化の都合でドメインオブジェクトが変更されることはあってはなりません。逆に、ドメインオブジェクトの都合で永続化の方法やIFを変更することは当然です。

Repository

いよいよRepositoryの作成です。

今回はデータの保存ですので、saveというメソッドをもつRepositoryを作ってみましょう。

package chalkboard.me.bulletinboard.domain.model;

public interface UserCommentRepository {
  void save(UserComment userComment);
}

Repositoryは永続化の抽象です。
なので、具体的な実装ではなくインタフェースとして実現されます。

具体的な実装はinfrastructure層に作成しましょう。
UserCommentDatasourceクラスを作成します。

実装はこのようになります。

package chalkboard.me.bulletinboard.infrastructure.datasource;

import chalkboard.me.bulletinboard.domain.model.UserComment;
import chalkboard.me.bulletinboard.application.dto.UserCommentDto;
import chalkboard.me.bulletinboard.domain.model.UserCommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class UserCommentDatasource implements UserCommentRepository {
  @Override
  public void save(UserComment userComment) {
    
  }
}

では、saveメソッドの中身の実装に移りましょう。
DBへ値を書き込むには、MyBatisの機能と連携を取る必要があります。

MyBatis

MyBatisはJavaでよく使われる永続化フレームワークの一つです。 他の選択肢として、Hibernateがあります。
自分で設定したら大変に面倒くさいDBとの連携をスムーズに行ってくれます。

設定

簡単にですが、MyBatisおよびmybatis-thymeleafの設定を行いましょう。

resources配下にmybatisそのものの設定ファイルを作成します。

内容はこのようになります。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="defaultScriptingLanguage" value="org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriver"/>
        <setting name="jdbcTypeForNull" value="NULL" />
        <setting name="defaultStatementTimeout" value="600"/>
    </settings>
</configuration>

configの詳細はこちらのリンクを参照してください.

Mapper

MyBatisはDBとの連携、SQLの実行に関してMapperという機能でサポートを行います。
Mapperをどのように作成するか、体験してみましょう。

UserCommentMapperインタフェースを作成します。

内容はこのようになります。

package chalkboard.me.bulletinboard.infrastructure.datasource;

import chalkboard.me.bulletinboard.application.dto.UserCommentDto;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserCommentMapper {
  @Insert("sql/insertUserComment.sql")
  void insert(@Param("dto") UserCommentDto dto);
}

@Insert("sql/insertUserComment.sql") は利用するSQLファイルを指しています。
MyBatisのMapperはこのように、実行SQLファイルとJavaコードのマッピングに使用されます。

さて、SQLファイルを作成しましょう。

resourcesディレクトリに下記のように作成します。

内容はこの通りです。

INSERT INTO USER_COMMENT(NAME, MAILADDRESS, TEXT) VALUES (
  /*[# mb:p="dto.name"]*/ 'name' /*[/]*/,
  /*[# mb:p="dto.mailAddress"]*/ 'mailaddress' /*[/]*/,
  /*[# mb:p="dto.comment"]*/ 'text' /*[/]*/
);

INSERT文はSQLにおいて、データの保存を担当します。
構文はDB毎に微妙に異なり、今回はH2の構文に従っています。

このSQLファイルの記法は、mybatis-thymeleafを利用している点で、通常のmybatisとは異なります。
mybatis-thymeleafを使うとXMLを利用せずSQLとJavaコードを結び付けられますが、それはこのように、SQLファイルを動的にthymeleafにより動的にレンダリングすることで可能にしています。

/*[# mb:p="dto.name"]*/ 'name' /*[/]*/
は、Thymeleafのテキスト構文です。
'name'の部分はテンプレート部分であり、mb:pに指定されている"dto.name"の値で書き換えられます。

mb:pは、mybatis-thymeleafの、thymeleaf拡張です

ユースケースを完成させる

ようやくパーツは揃いました。つなげていきましょう。

まず、未完成のRepositoryの具体実装、UserCommentDatasourceを完成させましょう。

package chalkboard.me.bulletinboard.infrastructure.datasource;

import chalkboard.me.bulletinboard.domain.model.UserComment;
import chalkboard.me.bulletinboard.application.dto.UserCommentDto;
import chalkboard.me.bulletinboard.domain.model.UserCommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class UserCommentDatasource implements UserCommentRepository {
  private final UserCommentMapper mapper;

  @Override
  public void save(UserComment userComment) {
    mapper.insert(UserCommentDto.from(userComment))
  }
}

これで完成です。 
UserCommentMapper mapperをDI(依存性注入)するように指定します。
このようにしておけば、SpringがMyBatisの提供する機能を詰め込んでくれます。

次に、UserCommentUseCaseクラスを完成させましょう。

package chalkboard.me.bulletinboard.application.usecase;

import chalkboard.me.bulletinboard.application.form.CommentForm;
import chalkboard.me.bulletinboard.domain.model.UserComment;
import chalkboard.me.bulletinboard.domain.model.UserCommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserCommentUseCase {
  private final UserCommentRepository repository;

  /**
   * ユーザの書き込みをDBに反映し、表示するデータをプレゼンテーション層に渡す
   * @param commentForm ユーザの入力データ
   * @return 表示するデータ
   */
  public void write(CommentForm commentForm) {
    // フォームオブジェクトからドメインオブジェクトへ変換
    UserComment userComment = UserComment.from(
        commentForm.getName(),
        commentForm.getMailAddress(),
        commentForm.getComment()
    );

    // 例えばここで、直近の投稿の一覧を取得し、今回と同じ内容の投稿がないかチェックする

    repository.save(userComment);
  }
}

このようになります。

先程と同じように、UserCommentRepositoryをDIさせます。
UserCommentRepositoryを実装しているのはUserCommentDatasourceだけですから、SpringはUserCommentDatasourceのインスタンスを注入します。

そして、このユースケースをControllerから呼び出しましょう。

package chalkboard.me.bulletinboard.presentation;

import chalkboard.me.bulletinboard.application.form.CommentForm;
import chalkboard.me.bulletinboard.application.usecase.UserCommentUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequiredArgsConstructor
public class BoardController {
  private final UserCommentUseCase userCommentUseCase;

  @GetMapping("/board")
  public ModelAndView viewBoard(ModelAndView modelAndView){
    modelAndView.setViewName("board");
    modelAndView.addObject("commentForm", new CommentForm());
    return modelAndView;
  }

  @PostMapping("/board")
  public ModelAndView postComment(
      @Validated @ModelAttribute CommentForm comment,
      BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
      ModelAndView modelAndView = new ModelAndView("/board");
      modelAndView.addObject("commentForm", comment);
      return modelAndView;
    }
    // エラーが無ければ保存する
    userCommentUseCase.write(comment);
    return new ModelAndView("redirect:/board");
  }
}

今度は、UserCommentUseCaseをDIしています。
このように、@Repository,@Serviceといったアノテーションを持つクラスはSpringによってDIさせることが可能です。

動かしてみる

SpringBootを起動してみましょう。

起動すると、H2の機能により、h2/schema.sqlが実行され、DBが初期化されます。

適当にいれて、Formを送信してみましょう。

何も起きませんね。
これは当然で、データの取得処理を実装していないから、です。

http://localhost:8080/h2-console
にアクセスしてみましょう。

JDBC URLをjdbc:h2:mem:develop
に書き換えてください。これで、Connectします。

すると、下記のような画面になります。これは、現在掲示板アプリとつながっているH2データベースのデバッグツールです。

左カラムのUSER_COMMENT1回クリックしましょう。

右テキストエリアの内容が、画像と同じSELECT * FROM USER_COMMENTになっているか確認してください。
なっていなければ、SELECT * FROM USER_COMMENTと上書きしてください。
Runタブを押してください。これで、このSQLが実行されます。

ページ下部に、送ったデータが表示されたら成功です。
データベースにユーザから送られたデータを保存することができました。

注意

DBの内容は、SpringBootを起動し直す度に全削除されます。

今回のPR

https://github.com/angelica-keiskei/spring-sample/pull/5