SpringBoot JPARepository 使ってみた

Java

DBとの接続はWebアプリでは避けて通れないわけですが、

あまりよくわからないまま、JDBC、mybatisあたりを使っていたので今回は、DBの操作を行うJPAってのを使ってみたいと思います。

JPAとは!

Java Persistance APIの略で、

RDBのデータを扱うJavaEEアプリを開発するためのフレームワーク。。

らしいです!笑

Java Persistence API - Wikipedia

O/R MappingやDAOの技術仕様らしく、それを実装したものが

・EclipseLink

・Hibernate ORM

・Apache Open JPA

みたい。

この人のがわかりやすかったです。

JPAの基礎1 - Qiita
JPAの概要JPAとは?JPAは「Java Persistence (JSR 338)」の略称であり、Java EE標準のO/R MappingおよびDAOの技術仕様です(元々の名称は「Java…

Hibernateってよく聞くけど、これはJPA仕様を実装したO/R Mapperやったんですね!勉強になります!

ところで、O/R Mappingとはって話ですが、これは私なりにかいつまんで説明すると、Object / Relational Mappingの略で、JavaでいうオブジェクトとRDBでいうテーブルを紐付けましょう!って思想。

あとで紹介するソース見てもらったら早いんですが、Javaのクラス自身のフィールドにRDBのテーブルのカラムを持ってます。で、データをセレクトすると勝手にインスタンスが生成されて(もちろんフィールドには、取得したカラムの値がばっちし入ってます)取得できるって感じ。

めちゃくちゃすげーやん!!

でね。こいつを使いやすくするための機能をSpringBootは持ってはるんすわ!

やるね!

では、実際に作ってみよう!

今回は、DBを別途用意するんめんどかったんでJavaライブラリとして利用できるH2を使ってデータ永続化操作をやっていきたいと思います!

ちなみに、ビルドツールにはMavenを使ってやりますんで、Gradle使ってる人は読み替えてください。

では早速、やってきますか!

手順は以下の通りです

1:dependencyの追加

2:RDBのテーブルに当たるJavaクラス(Entityクラス)を作成

3:リポジトリを作成する

4:リポジトリを利用する

5:テンプレートを作成する

1:dependencyの追加

pom.xmlに次のdependencyを追加していきます

「JPA」「Thymeleaf」「Web」「H2」

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

2:RDBのテーブルに当たるJavaクラス(Entityクラス)を作成

package com.soloware.jpa.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.lang.NonNull;

@Entity
@Table(name="MY_DATA")
public class MyData {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column
    @NonNull
    private long id;

    @Column(length = 50, nullable = false)
    @NotEmpty
    private String name;

    @Column(length = 200, nullable = true)
    @Email
    private String mail;

    @Column(nullable = true)
    @Min(0)
    @Max(200)
    private Integer age;

    @Column(nullable = true)
    private String memo;

    /**
     * @return the id
     */
    public long getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(long id) {
        this.id = id;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the mail
     */
    public String getMail() {
        return mail;
    }

    /**
     * @param mail the mail to set
     */
    public void setMail(String mail) {
        this.mail = mail;
    }

    /**
     * @return the age
     */
    public Integer getAge() {
        return age;
    }

    /**
     * @param age the age to set
     */
    public void setAge(Integer age) {
        this.age = age;
    }

    /**
     * @return the memo
     */
    public String getMemo() {
        return memo;
    }

    /**
     * @param memo the memo to set
     */
    public void setMemo(String memo) {
        this.memo = memo;
    }
}

ざっと、長くなってますが、Getter/Setterのせいです。笑

Lombok使うと省略できるみたいやけど、今回はなしで!

クラスに@Entityっていうアノテーションをつけます。

その下に@Tableアノテーションありますが、もしクラス名がテーブル名と一致する場合、つけなくても良いらしい。今回は違うのでつけてます。

@Idアノテーションは主キーをさします。

その下にある@GeneratedValueってのは、主キーのフィールドに対して値を自動生成するためのアノテーションです。

strategy=GenerationType.AUTOとすることで、自動で値を割り振ってくれます。

@Columnアノテーションは、フィールド名がカラム名と一緒の場合は、省略できます。

そのほかの@NotNullや、@NotEmptyなんかは、あとで出てくるバリデーションチェックの時に詳しく説明します。それぞれnullであったり、空はダメって感じの意味のアノテーションです。

3:リポジトリを作成する

こいつは、データベースアクセスのための基本的な、ほんまに基本的な手段を提供してくれるものです。

より高度なデータベースアクセスの手段を提供してくれるEntityManagerっていうのもあるみたいやけど、そいつは今回ノータッチで。

このリポジトリですが、データベースアクセスの処理(汎用的なCRUDですね)を自動的に生成し、実装してくれます。

なので、コードはほとんど書きません笑

ではでは、リポジトリとはどんなもんかみてみましょう。

package com.soloware.jpa.repository;

import java.util.Optional;

import com.soloware.jpa.entity.MyData;

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

@Repository
public interface MyDataRepository extends JpaRepository<MyData, Long>{

    public Optional<MyData> findById(Long id);
}

はい、これだけー

注目すべきは、JpaRepositoryインターフェースを継承しているところです。

ジェネリックスにMyDataクラスとLongクラスを指定しています。

MyDataクラスを紐付けまっせーってことと、主キーの型はLongでっせーてこと。

@Repositoryアノテーションは、stereotypeのアノテーションの1つで、このクラスがデータベースアクセスのクラスですよーって明示しているだけです。

※他のstereotypeのアノテーションには、@Controllerや、@Service、@Componentなんかがあって、それぞれ「リクエストハンドラー」「業務ロジック」「それ以外(データベースアクセスでもない)」って感じです。

なんで、別に@Controllerアノテーションをつけても構わないとは思います笑

ソース見た時に気持ち悪すぎますが。。。

っでこのfindByIdってメソッドが、引数にもらったIdで検索しまっせ!ってこと。

あ、これはフィルタリング条件にしたいカラムがNameだったら、findByNameってすりゃいいし、IdとNameとかだったら、findByIdAndNameってやればいいです。

ちなみに、こういったテーブルのカラムによってメソッド名が変わるものについては、このRepositryインターフェースにメソッドを定義(実処理はいらない)しないといけませんが、findAll(全件検索)とかだと、実装すりゃしなくていい。なので、このリポジトリ使ってfindAllメソッドを呼ぶことができる。

あとで見せますねー

4:リポジトリを利用する

では、作成したリポジトリインターフェースを使ってみよう!

リクエストハンドラークラスから直接使っちゃおうと思います。

※商用では、ちゃんとビジネスロジック側で使ってください。

package com.soloware.jpa.controller;


import javax.annotation.PostConstruct;

import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.soloware.jpa.entity.MyData;
import com.soloware.jpa.repository.MyDataRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class MainController{

    @Autowired
    MyDataRepository repository;

    @PostConstruct
    public void init(){
        MyData d1 = new MyData();
        d1.setName("Soloware1");
        d1.setAge(10);
        d1.setMail("soloware@mail.com");
        d1.setMemo("This is Soloware1");
        repository.saveAndFlush(d1);

        MyData d2 = new MyData();
        d2.setName("Soloware2");
        d2.setAge(20);
        d2.setMail("soloware2@mail.com");
        d2.setMemo("This is Soloware2");
        repository.saveAndFlush(d2);

        MyData d3 = new MyData();
        d3.setName("Soloware3");
        d3.setAge(30);
        d3.setMail("soloware3@mail.com");
        d3.setMemo("This is Soloware3");
        repository.saveAndFlush(d3);
    }

    @RequestMapping(value="/", method=RequestMethod.GET)
    public String index(
                    @ModelAttribute("formModel") MyData myData,
                    Model model){
        model.addAttribute("msg", "This is sample content");
        Iterable<MyData> list = repository.findAll();
        model.addAttribute("datalist", list);
        return "index";
    }

    @RequestMapping(value="/", method=RequestMethod.POST)
    @Transactional(readOnly = false)
    public String form(
                    @ModelAttribute("formModel") 
                    @Validated MyData myData,
                    BindingResult result,
                    Model model){
        if(!result.hasErrors()){
            repository.saveAndFlush(myData);
            return "redirect:/";
        }else{
            model.addAttribute("msg", "Error!!");
            Iterable<MyData> list = repository.findAll();
            model.addAttribute("datalist", list);
            return "index";
        }
    }
}

はい、おなかいっぱいです。。。

上から咀嚼していきたいと思います。

まず、stereotypeのアノテーション@Controllerはいいですね。

このクラスはリクエストハンドラーです。

次の@Autowiredで作成したMyDataRepositoryをDIしています。

ここで、疑問が出ますね。MyDataRepositoryってインターフェースちゃうん?!っと。

Springフレームワークのすごいとこなんやね。これが。

SpringMVCによって、インターフェースに必要な処理が組み込まれた無名クラスのインスタンスが作成され、それが設定されてるんです!すごない?!

次の@PostConstructですが、こいつはサブ要素なんでほぼスルーでもいいです。

何かと言うと、このアノテーションがついているメソッドは、コンストラクタによってインスタンスが生成された後に呼び出されるってことを意味していて

@Controllerアノテーションをデフォルトスコープで実装しているこのMainControllerクラスは、シングルトンなので起動時に一度だけインスタンスが生成されます。したがってそのインスタンス生成時に一度だけ呼ばれるメソッドということになります。そのメソッド内でデータを作ってあらかじめDBに入れてるってだけです。

このsaveAndFlushメソッドが引数に渡したEntityクラスをDBに登録するメソッドです。

次、@RequestMappingアノテーションですが、こいつはvalueに設定しているパスに、methodに設定しているHTTPメソッドでリクエストがきた場合に、処理をさせるようにURLマッピングをさせるものです。

で、indexメソッドの引数にある@ModelAttributeですが、こいつは画面側を作る際に説明します。

あ、ちなみにThymeleafの使い方とかそこへの値の渡し方とかは、本記事の守備範囲外なので、知らない人は調べておいてください。

はい、ではindexメソッドの中身です。

出ました!findAllメソッド!

DIしたMyDataRepositoryのfindAllメソッドを呼んでDB内のデータを全て取得してきてます。

取得したものは複数件存在するため、Iterable型です。

最後に取得したデータをdatalistというキー名でテンプレート側に渡して終わり。

2つ目のformメソッドは、POSTされた時に呼ばれます。

引数の@ModelAttributeはテンプレート側作成時に説明するので割愛。

@Validatedですが、これは、@Entityを作成した時を思い出してください。

そう!@NotNullや、@NotEmptyってありましたよね。これらのアノテーションがついたフィールドがnullであったり空かをチェックしてくれるアノテーションが@Validatedです!

で、このチェック結果が次の引数BindingResultに格納されます。

なので、formメソッドの中身は、最初のBindingResult結果のチェックから始まります。

result.hasErrors()で1つでもエラーがあればtrue が返ってきます(複数バリデーションチェック用のアノテーションが存在するため)

エラーがなかったら、データをDBに登録して、あった場合は再度DBからデータを全件取得してindex.htmlを表示します。

最後に、重要なアノテーションの説明。

@Transactionalです。DBにデータを格納するためreadOnly=falseとしてトランザクションを張ってます。データ更新が入る部分はトランザクション忘れずに!

5:テンプレートを作成する

では、ラストにテンプレートを作成して動かしてみましょう!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title th:text="#{app.indexTitle}">title</title>
    <style>
    .err{
        color:red;
    }
    </style>
</head>
<body>
    <h1 th:text="#{app.indexTitle}">Hello Page</h1>
    <p th:text="${msg}"></p>
    <table>
        <form action="/" method="POST" th:object="${formModel}">
            <tr>
                <td><label for="name">名前</label></td>
                <td><input type="text" name="name" th:value="*{name}" th:errorclass="err"/>
                <div th:if="${#fields.hasErrors('name')}" th:errorclass="err" th:errors="*{name}"></div>
                </td>
            </tr>
            <tr>
                <td><label for="age">年齢</label></td>
                <td><input type="text" name="age" th:value="*{age}" th:errorclass="err"/>
                    <div th:if="${#fields.hasErrors('age')}" th:errorclass="err" th:errors="*{age}"></div>
                </td>
            </tr>
            <tr>
                <td><label for="mail">メール</label></td>
                <td><input type="text" name="mail" th:value="*{mail}" th:errorclass="err"/>
                    <div th:if="${#fields.hasErrors('mail')}" th:errorclass="err" th:errors="*{mail}"></div>
                </td>
            </tr>
            <tr>
                <td><label for="memo">メモ</label></td>
                <td><textarea name="memo" th:text="*{memo}" cols="20" rows="5"></textarea></td>
            </tr>
            <tr>
                <td><input type="submit"/></td>
            </tr>
        </form>        
    </table>
    <hr>
    <table>
        <tr><th>ID</th><th> 名前</th></tr>
        <tr th:each="obj : ${datalist}">
            <td th:text="${obj.id}"></td>
            <td th:text="${obj.name}"></td>
        </tr>
    </table>
</body>
</html>

注目すべきはformタグの「th:object=”${formModel}”」です!

このformModelって文字列、どこかで見たことありますよね。

そうです。MainControllerのメソッドの引数にあった「@ModelAttribute(“formModel”) MyData myData」の部分です!

このアノテーションで指定したformModelをテンプレート側で「th:object」で指定することで、MyDataクラスのフィールド変数の値を参照することができるっていうカラクリです。ちなみに、フィールド変数を参照するときは「*{フィールド変数名}」となります。

GETアクセス時は、MyDataの引数にはnewされたインスタンスが作成されて割り当てられるので、値は何も入っていません。

しかし、POSTアクセス時のMyDataには、送信されたフォームの値が自動的にMyDataインスタンスの変数に格納されて引数として渡される。

パラメータをいちいち個別に取得しなくていいし、バリデーションチェックもアノテーションでできるし、めちゃ便利ですよね!!

あと、th:errorclassってのは、バリデーションチェックでエラー判定になった場合に、ここで指定したものがclass属性の値としてHTMLタグを作成するものです。

で、th:if=”${#fields.hasErrors(‘name’)}”ってのが、th:objectで取得したMyDataクラスのフィールドのnameって変数がバリデーションチェックでエラーとなっているかの条件分岐です。エラーになっている場合、このdivタグは表示されます。

以上でざっとコードの説明は終わりです。

では、早速動かしてみましょう!

私は、VSCodeでSpringBootプロジェクトを作成しているので、

./mvnw clean packageコマンドでビルドして、target内のjarファイルを

java -jar で実行します。

初期画面はこんな感じです。

ライン下側が、@PostConstructで初期登録したデータです。

普通に登録してみましょう!

はい!無事に登録できてます!

では、次にあえてバリデーションで引っかかってみましょう!

    @Column(length = 50, nullable = false)
    @NotEmpty
    private String name;

    @Column(nullable = true)
    @Min(0)
    @Max(200)
    private Integer age;

これなんで、nameの部分を空白にして、ageの部分を201以上にしてみます。

無事に引っかかりました!笑

ちなみに、このエラーメッセージの文言ですがこれも自分で決めれます。

やり方は、resourcesフォルダ内に「ValidationMessages.properties」というファイルを作成します。中身はこんな感じです。

org.hibernate.validator.constraints.NotBlank.message = \u7a7a\u767d\u306f\u4e0d\u53ef\u3067\u3059\u3002
org.hibernate.validator.constraints.NotEmpty.message = \u7a7a\u767d\u306f\u4e0d\u53ef\u3067\u3059\u3002
javax.validation.constraints.Max.message = {value} \u3088\u308a\u5c0f\u3055\u304f\u3057\u3066\u304f\u3060\u3055\u3044
javax.validation.constraints.Min.message = {value} \u3088\u308a\u5927\u304d\u304f\u3057\u3066\u304f\u3060\u3055\u3044
org.hibernate.validator.constraints.Email.message = \u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002

はい、なんじゃこりゃ!ですね笑

ていうのもこの辺のファイルを日本語で書いてしまうと文字化けしてしまうので、ascii変換しないといけないんです。

変換コマンドはこんな感じ

native2ascii 日本語が書いてあるファイル名 変換後のファイル名

ちなみに変換前 がこれ

org.hibernate.validator.constraints.NotBlank.message = 空白は不可です。
org.hibernate.validator.constraints.NotEmpty.message = 空白は不可です。
javax.validation.constraints.Max.message = {value} より小さくしてください
javax.validation.constraints.Min.message = {value} より大きくしてください
org.hibernate.validator.constraints.Email.message = メールアドレスではありません。

今回の場合は、日本語が書いてあるファイル名と変換後のファイル名は同じValidationMessages.propertiesになります。

で、中身の説明は、するまでもないですね。各アノテーションで引っかかった場合に出力するmessageに日本語を入力しているだけです。

駆け足で説明してきたけど、個人的には、SQLとJavaのオブジェクトを紐付けるmybatisの方が好きです。自分でSQL文を書けるし、推論はできるけど別途、JPAのメソッド覚えるのしんどかった笑

しかも、JPAやとSQLは勝手に生成されてまうから(自分でSQLを書く方法もあるみたい。。でも、それやったらJPA使うメリットなくね?)、JOINとかしたかったらI/Oが多くなりそうやし。。

あ、でも私みたいなんは、そこまでSQL詳しくないから、大人しくJPA使っとくのもありか。。笑

ってことで、次回はmybatisについて書いてみたいと思いまーす

BYE

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