Help us understand the problem. What is going on with this article?

SpringBoot2チートシート

はじめに

 Springを業務で使う筆者が、「SpringBoot2 プログラミング入門」を勉強して、まとめたチートシート・読書メモのようなもの。
 対象読者は、Javaの基礎が身についていて、SpringBoot未経験者〜基礎を勉強し始めた位の層。
 言語はJava、
 ビルドツールはMaven、
 テンプレートエンジンはThymeleafです。

SpringBootとは

 SpringFrameworkの提供するライブラリとSpringMVC(フレームワーク)を高速なWEBアプリ開発向けに組み合わせてできたフレームワーク(群)

1. Spring開発のセットアップ

 本記事をチートシート的用途とするため、ここでは割愛。

2. Groovyによる超簡単アプリケーション開発

 GroovyはJavaでの本格的な開発前にプロトタイプを短時間で作ることに向いている言語。Groovyについてはここでは割愛。

@ RestController

 外部からアクセスして必要な情報などを取り出すシステムを構築するのに用いられる。
アクセスしたら必要な情報などをテキストやXML形式で送り返すといった用途。

@ RequestMapping

 このアドレスにアクセスされたらこのメソッドを実行するといった意味。
@RequestMapping("/")のように表記し、引数にパスを指定する。
指定パスにアクセスしたらこのアノテーションがつけられたメソッドが実行されるようになる。

SampleController.java
@RestController
public class SampleController {

  @RequestMapping("/")
  public String index() {
    return "Hello SpringBoot World";
  }
}

 Thymeleafを利用する

 HTMLのタグに、追記して使うことができるテンプレートエンジン。
特徴は、

  • タグの中に「th:◯◯」の独自属性を用意
  • 「${}」の形をとって変数名を記述して、その場所に値を埋め込む
home.html
<body>
  <h1>Hello!</h1>
  <p class="msg" th:text="${msg}">this is sample.</p>
</body>

テンプレートエンジンとしてのGroovyでHTMLを書くことも可能だが、用途の都合上ここでは割愛。

@Controllerについては、3章に記載

@ ResponseBody

@ResponseBodyをつけたメソッドは戻り値がそのままレスポンスのコンテンツになる。
@RestControllerをつけたコントローラーのメソッドは、@ResponseBodyを付けなくても、戻り値がコンテンツになる。
@Controllerがついたコントローラークラス内のメソッドでは、ModelAndViewクラスを返すことが多いが、例外的に、JSONやXMLを返す必要があるときに、@ResponseBodyをコントローラーに付けることで、コンテンツそのものを返すことができる。

3. JavaによるSpringBoot開発の基本

SpringBootアプリケーションの仕組み

 SpringBootは内蔵されているJavaサーバーを使ってアプリを実行するので、デプロイが不要。
 クラウドサービスのような、サーバー内でプログラムを実行するものに相性がいい。
  ファイルサイズが肥大化するデメリットもあるが、JavaでWeb開発を行うデファクトスタンダードになりつつある。

Spring Starter Projectのpom.xml

parentの定義は以下

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.3.0.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

dependenciesの定義は以下

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

buildおよびpluginの定義は以下

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

RestControllerを利用する

Spring Tool Suite(以下、STS)でSpringStarterプロジェクトを作成すると、デフォルトでMyBootAppApplication.javaというファイルが作成される。
 @ SpringBootApplicationをつけると、SpringBootの起動時に呼び出されるクラスになる。
 アノテーションをつけたクラスをSpringApplication.runすると、アプリが起動する。
 「run」はアプリケーションを起動するためのメソッド。引数には、実行するクラスのClassインスタンスと、パラメータとして渡すデータを用意する。

MyBootAppApplication.java
@SpringBootApplication
public class MyBootAppApplication {

  public static void main(String[] args) {
    SpringApplication.run(MyBootAppApplication.class, args);
  }
}

MVCアーキテクチャについて

アーキテクチャ 役割
Model アプリケーションで使うデータを管理する
View 画面の表示を扱う
Controller 全体の処理の制御を行う

パラメータを渡す

@RequestMapping("/{param}")の形でパス変数を利用することで、アドレス以下のパス部分に渡された値を変数として受け取ることができる
@RequestMappingとは

パス変数と@ PathVariable

 パス変数によって値が渡されるものであることを示す。

@RequestMapping("/{num}")
public String index(@PathVariable int num){
  int res = 0;
  for(int i = 1; i<= num; i++){
    res += i;
  }
  return "total:" + res;
}

オブジェクトをJSONで出力する

RESTのサービスはString以外の戻り値として、クラスを戻すこともできる。returnされたインスタンスは、JSON形式に変換される。

//DataObjectクラスを別途定義しておく
public DataObject index(@PathVariable int id) {
  return new DataObject();
}

ControllerによるWebページ制作

 通常のWebページを利用する場合は、コントローラークラスの前に@Controllerを付ける。
本記事においては、テンプレートにThymeleafを使用したいので、以下のようにして、依存関係をpom.xmlに追加する。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

@ Controller

 テンプレートを利用してHTMLページをレンダリングし、表示するのに用いられる。

SampleController.java
@Controller
public class SampleController {
  @RequestMapping("/")
  public String home(){
    return "home";
  }
}

Modelクラスの利用

 ModelはWebページで利用するデータを管理するためにクラス。
このModelにテンプレートで利用する値を設定しておくことで、データを渡すことができる。
addAttributeで値を設定する。第1引数には値の名前を、第2引数には補完する値をそれぞれ指定する。
以下のサンプルに倣うと、テンプレート側で${msg}という形で変数msgの値を取り出すことができる。
戻り値をModelとして使うことができない(テンプレートの情報を持っていないから)。

@RequestMapping("/{num}")
public String home(@PathVariable int num, Model model) {
  ...
  model.addAttribute("msg", "total:" + res);
  return "home";
}

ModelAndViewクラスの利用

 テンプレートで利用するデータ類と、ビューに関する情報(テンプレート名など)を管理する。このModelAndViewを戻り値として返すことで設定されたテンプレートを利用するようになる。オブジェクトをaddObject(String entitySetName, Object entity)というように設定し、使用するビューの名前をsetViewName(String viewName)で設定する。

@RequestMapping("/{num}")
public ModelAndView index(@PathVariable int num, ModelAndView mav) {
  ...
  mav.addObject("msg",  "total:" + res);
  mav.setViewName("home");
  return mav;
}
  • Model
     テンプレートで利用するデータ類をまとめて管理するものであるが、ビュー関連の情報は持っていないので、Modelを戻り値として使うことはできない。

  • ModelAndView
    テンプレートで利用するデータ類と、ビューに関する情報をすべてまとめて管理する。ModelAndViewを戻り値として返すことで設定されたテンプレートを利用するようになる。

 

フォームを利用する

 <form method="POST" action="/">でフォームを用意、送信先は同じ"/"で、POST送信する。input type="text"タグでは、th:text="${value}"のようにして、$valueの値を入力フィールドに表示させている。
 @RequestMapping(value="/", method=RequestMethod.POST)のように@RequestMappingの引数が複数ある場合は、マッピング先をvalue="/"のように引数名を省略せずに書く必要がある。サンプルは同じアドレスで、メソッドについてGETPOSTで区別している。
 フォームから送信された値は、sendメソッドで処理している。@RequestParamは、フォーム送信された値を指定するためのアノテーションで、これによってフォームのname="text1"に入力された値がこの引数strに渡される。

【コンロトーラー側】

SampleController.java
@Controller
public class SampleController {
  @RequestMapping(value="/", method=RequestMethod.GET)
  public ModelAndView index(ModelAndView mav) {
    mav.setViewName("form");
    mav.addObject("msg", "please put and send your name.");
    return mav;
  }

  @RequestMapping(value="/", method=RequestMethod.POST)
  public ModelAndView send(@RequestParam("text1")String str, ModelAndView mav) {
    mav.addObject("msg", "こんにちは");
    mav.addObject("value", "message");
    mav.setViewName("form");
    return mav;
}

【テンプレート側】

form.html
<body>
    <h1>Sample Form</h1>
    <p th:text ="${msg}">ここはmsgと置き換わる</p>
    <form method="POST" action="/">
        <input type="text" name="text1" th:value="${value}"/>
        <input type="submit" value="Click" />
    </form>
</body>

その他フォームコントロールテンプレート例

 フォームから送信された値はsendメソッドで受け取っている。メソッドにはフォームからの値を受け取れるように、@RequestParamをつけた引数が4つ用意されている。valueでパラメータの名前を指定し、requiredで値のnull制約を指定する。
【コントローラー側】

SampleController.java
@Controller
public class SampleController {
    @RequestMapping(value="/", method=RequestMethod.GET)
    public ModelAndView index(ModelAndView mav) {
    //GETリクエスト省略
    }

    @RequestMapping(value="/", method=RequestMethod.POST)
    public ModelAndView send(
        //チェックボックス:値は選択状態をboolean値として得ることができる
        @RequestParam(value="check1", required=false)boolean check1,
        //ラジオボタン:選択した項目のvalueをString値として渡すが未選択の場合はnull
        @RequestParam(value="radio1",required=false)String radio1,
        //選択リスト:単一項目の選択時はvalueをString値として渡すが、複数項目が選択できる場合はString配列として渡し、未選択の場合null
        @RequestParam(value="select1", required=false)String select1,
        @RequestParam(value="select2", required=false)String[] select2, 
        ModelAndView mav) {
      String res = "";
      //処理省略
      ...
      //resをmsgに埋め込む
      mav.addObject("msg", res);
      mav.setViewName("form");
      return mav;
    }
}

【テンプレート側】

<body>
    <h1>Form Controll Example</h1>
    <form method="POST" action="/">
        <div>
        <input type="checkbox" id="check1" name="check1" />
        <label for="check1">チェックボックス</label>
        </div>
        <div>
        <input type="radio" id="radioA" name="radio1" value="male" />
        <label for="radioA">男性</label>
        </div>
        <div>
        <input type="radio" id="radioB" name="radio1" value="female" />
        <label for="radioB">女性</label>
        </div>
        <div>
        <select id="select1" name="select1" size="4">
            <option value="Red"></option>
            <option value="Blue"></option>
            <option value="Yellow"></option>
        </select>
        <select id="select2" name="select2" size="4" multiple="multiple">
            <option value="SmartPhone">スマホ</option>
            <option value="Tablet">タブレット</option>
            <option value="Laptop">ラップトップ</option>
        </select>
        </div>
        <input type="submit" value="Click" />
    </form>
</body>

リダイレクト

 あるアドレスにアクセスしたとき、必要に応じて別のアドレスに移動させたいケースのやり方としてフォワードリダイレクトがある。

  • フォワード:サーバー内部で別のページを読み込み、表示する。アクセスするアドレスはそのままで、表示内容だけが別のページに差し替えられる。
  • リダイレクト:クライアント側に送られた後で別のページに移動させる。なのでアクセスしているアドレスも移動先のものに変更される。

【コントローラー側】

SampleController.java
@Controller
public class SampleController {
    @RequestMapping("/")
    public ModelAndView index(ModelAndView mav) {
        mav.setViewName("home");
        return mav;
    }
    //"/other"はアドレス自体が、"/"に変更されて、表示内容も"/"のものになる。 
    @RequestMapping("/other")
    public String other() {
        return "redirect:/";
    }
    //"/home"にアクセスすると、アドレスはそのままに、表示内容が"/"のものを見せる。
    @RequestMapping("/home")
    public String home() {
        return "forward:/";
    }
}

【テンプレート側】

home.html
<body>
  <h1>Home Page</h1>
</body>

4. テンプレートエンジンを使いこなす

Thymeleafいろいろ

ユーティリティオブジェクト

 よく使われるクラスを「#名前」という定数として定数式の中に直接記述して利用できる。クラスの定数なので、直接クラスメソッドなどを呼び出して利用することができる。「#dates.クラスメソッド」というように。以下例

ユーティリティオブジェクト 定数
#strings Stringクラスの定数
#numbers Numberクラスの定数
#Bools Booleanクラスの定数
#dates Dateクラスの定数
#objects Objectクラスの定数
#arrays Arrayクラスの定数
#lists Listクラスの定数
#sets Setクラスの定数
#maps Mapクラスの定数

Dateクラス定数#datesformatメソッドを使い、第1引数にDateクラスのオブジェクトを作成し、第2引数でyyyy/MM/dd HH:mm:ssという形式を指定。

    <p th:text="${#dates.format(new java.util.Date(), 'dd/MMM/yyyy HH:mm')}"></p>

Numberクラスの定数#numbersformatIntegerメソッドを使い、第1引数に整数を、第2引数の桁表示で、第3引数でカンマ区切りにしている

    <p th:text="${#numbers.formatInteger(10000000, 3, 'COMMA')}"></p>

Stringクラスの定数#stringstoUpperCaseは引数のテキストをすべて大文字に変換するメソッド

    <p th:text="${#strings.toUpperCase('show uppercase')}"></p>

パラメータへのアクセス

{param.id}のように書くことで、「id=○○」のような形で送られてきた値を受け取り、コントローラー通さず、直接テンプレート内でパラメータを利用することができる。ただし、得られる値は配列なので、値を取り出して使う。

<!--/?id=hoge&name=fugaのようにして渡す-->
<p th:text="'from parameter.. id=' + ${param.id[0] + ',name=' + param.name[0]}"></p>

メッセージ式

プロパティファイルから値を取り出し、テンプレート内で利用する。記述方法は、#{値の指定}

message.properties
article.title=this is home page.
article.message=this message from properties.
<h1 th:text="#{article.title}"></h1>
<p th:text="#{article.message}"></p>

リンク式とhref

/?id=fugaとアクセスすると、linkには/hoge/fugaが設定される。

<p><a href="home.html" th:href="@{/hoge/{id}(id=${param.id})}">link</a></p>

選択オブジェクトへの変数式

変数式は、数値やテキストだけでなく、オブジェクトを利用することができる。やり方としては、オブジェクトを指定し、選択されたオブジェクト内の値を取り出すため*{value}を使う。

  <table th:object="${object}">
    <tr><th>ID</th><td th:text="*{id}"></td></tr>
    <tr><th>NAME</th><td th:text="*{name}"></td></tr>
    <tr><th>MAIL</th><td th:text="*{value}"></td></tr>
  </table>

リテラル置換

 テキストの前後を「|」で囲むと、変数式を直接書き込んで文字列結合ができる。

<p th:text="|My name is *{name}. Mailaddress is *{value}.|"></p>

HTMLコードの出力

 Thymeleafでは、変数式でテキストを出力する際、安全のためにHTMLタグを全てエスケープするので、
th:utextを使うことで、エスケープを解除できる。ただし、値にHTMLタグが含まれているとそれがそのまま機能してしまうので、ユーザーから送信された情報を元にテキストを作成する場合、XSSなどの攻撃に対して無防備になってしまう。

【コントローラー側】

@RequestMapping("/")
public ModelAndView index(ModelAndView mav) {
    mav.setViewName("home");
    mav.addObject("msg", "message 1<hr/>message 2<br/>message3");
    return mav;
} 

【テンプレート側】

<p th:utext="${msg}">message.</p>

構文・インライン・レイアウト

条件式

  • 三項演算子(条件文 ? 値1 : 値2)

 予め用意した真偽値の結果に応じて出力する内容を変更する。

【コントローラー側】

@RequestMapping("/{id}")
public ModelAndView index(@PathVariable int id,
        ModelAndView mav) {
    mav.setViewName("check");
    mav.addObject("id",id);
    mav.addObject("num",id % 2 == 0);
    mav.addObject("even","Even number!");
    mav.addObject("odd","Odd number...");
    return mav;
}

【テンプレート側】

check.html
<p th:class="${num} ? 'even' : 'odd'"></p>
  • th:if

 値として設定したものがtrue(ゼロ以外の数値、"0",
"off", "no"といった値以外のテキストも含む)の場合に、このタグ及びその内部にあるタグを表示する。

  • th:unless

値として設定されたものがfalse(数値のゼロ、"0", "off", "no"といったテキストも含む)の場合に、このタグ及び内部のタグを表示する。

【コントローラー側】

@RequestMapping("/{id}")
public ModelAndView index(@PathVariable int id,
        ModelAndView mav) {
    mav.setViewName("check");
    mav.addObject("id",id);
    mav.addObject("num",id >= 0);
    mav.addObject("trueVal","POSITIVE!");
    mav.addObject("falseVal","negative...");
    return mav;
}

【テンプレート側】

check.html
    <p th:if="${num}" th:text="${id} + ' is ' + ${trueVal}"></p>
    <p th:unless="${num}" th:text="${id} + ' is ' + ${falseVal}"></p>
  • th:switch

 指定された条件式の値をチェックし、その内側にあるth:caseから同じ値のものを探してそのタグだけを出力する。
 th:case="*"はワイルドカードで、どの条件にも当てはまらなかったものをキャッチする。

【コントローラー側】

@RequestMapping("/{month}")
public ModelAndView index(@PathVariable int month,
        ModelAndView mav) {
    mav.setViewName("index");
    int m = Math.abs(month) % 12;
    m = m == 0 ? 12 : m;
    mav.addObject("month",m);
    mav.addObject("check",Math.floor(m / 3));
    return mav;
}

【テンプレート側】

<div th:switch="${check}">
    <p th:case="0" th:text="|${month} - Winter|"></p>
    <p th:case="1" th:text="|${month} - Spring|"></p>
    <p th:case="2" th:text="|${month} - Summer|"></p>
    <p th:case="3" th:text="|${month} - Autumn|"></p>
    <p th:case="4" th:text="|${month} - Winter|"></p>
    <p th:case="*">...?</p>
</div>
  • th:each

 th:eachでは、配列やコレクションなどを値として用意し、value : ${list}のように拡張for文の要領で記述する。これにより、コロンのあとにあるリストから順に値を取り出して、コロンの前の変数に代入するようになる。

【テンプレート側】

<table>
    <tr>
        <th>NAME</th>
        <th>MAIL</th>
        <th>TEL</th>
    </tr>
    <tr th:each="obj:${data}">
        <td th:text="${obj[0]}"></td>
        <td th:text="${obj[1]}"></td>
        <td th:text="${obj[2]}"></td>
    </tr>
</table>

プリプロセッシング

 __${変数}__の形で宣言すると、事前に評価され、変数式が実行されるようになる。

【コントローラー側】

ArrayList<Pockemon> monsters = new ArrayList<>();
monsters.add(new Pockemon(0,"Hitokage","Fire"));
monsters.add(new Pockemon(1,"Fushigidane","Grass"));
monsters.add(new Pockemon(2,"Zenigame","Water"));
mav.addObject("num", 1);
mav.addObject("monsters",monsters);
return mav

【テンプレート側】

<div th:object="${monsters.get(${num})}">
    <p th:text="*{id}"></p>
</div>

インライン処理

th:textのような属性の形で用意しなくても(Thymeleafが機能していなくてもページ表示に影響を与えないようにしている面あり)、HTMLタグとタグの間に直接Thymeleafの変数式を書き込むことができる。タグにth:inline="text"のように記述するとその内部で[[${変数}]]のように変数式を書くことで、インライン処理が可能になる。

【コントローラー側】

ArrayList<Pockemon> monsters = new ArrayList<>();
monsters.add(new Pockemon(0,"Hitokage","Fire"));
monsters.add(new Pockemon(1,"Fushigidane","Grass"));
monsters.add(new Pockemon(2,"Zenigame","Water"));
mav.addObject("monsters",monsters);
return mav

【テンプレート側】

<tr th:inline="text" th:each="monster : ${monsters}">
    <td>[[${monster.id}]]</td>
    <td>[[${monster.name}]]</td>
    <td>[[${monster.type}]]</td>
</tr>

JavaScriptによるインライン処理

<script th:inline="javascript">inline"text"ではなく"javascript"と値を指定し、変数式は、/*[[${変数式}]]*/とコメントアウト形で記述することで、スクリプトの中に埋め込んで利用することができる。

<script th:inline="javascript">
   console.log(/*[[$(変数)]]*/);   
</script>

テンプレートフラグメント

 複数のファイルを組み合わせてページを構成するテンプレートフラグメントというやり方。ファイルの中にフラグメントとしてそこだけ切り取って扱えるようにした部品として記述されている部分を別のテンプレートの指定した場所にはめ込む。

【部品となるテンプレート】

part.html
<p th:fragment="frag_body">

【組み込む側】

<p th:include="part::frag_body">

そのほかのテンプレートエンジン

SpringにおいてはJSP, Groovyもテンプレートエンジンとして使用できるが都合上割愛

5. モデルとデータベース

 JavaではJPA(JavaPersistence API 永続化のためのAPI)を使ってDBを利用する。SpringにおいてもSpringBootStarterDataJPAを利用してDBアクセスを行う。

Mavenでプロジェクトを管理している場合、pom.xmlに、以下のdependencyを追加して使用する。

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

JPAを利用する場合、データとなる部分はエンティティと呼ばれるクラスとして定義される。DB内にテーブルを定義して、そこにレコードとしてデータを保管していくが、エンティティはこの一つ一つのレコードをJavaオブジェクトとして保管したものと考えていい。

以下、モデルとなるEntityクラスのサンプル

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column
    private long usrId;
    @Column(length = 50, nullable = false)
    private String usrName;
    @Column(length = 200, nullable = true)
    private String usrAddress;
    @Column(nullable = true)
    private Integer age;
    @Column(nullable = true)
    private String comment;

    public long getUsrId() { return usrId; }
    public void setUsrId(long usrId) { this.usrId = usrId; }

    public String getUsrName() { return usrName; }
    public void setUsrName(String usrName) { this.usrName = usrName; }

    public String getUsrAddress() { return usrAddress; }
    public void setUsrAddress(String usrAddress) { this.usrAddress = usrAddress; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public String getComment() { return comment; }
    public void setComment(String comment) { this.comment = comment; }
}

@ Entity

 エンティティクラスであることを示すアノテーション

@ Table(name="mydata")

 このエンティティクラスに割り当てられるテーブルを指定する。nameを省略した場合、クラス名がテーブル名として利用される。

@ Id

 プライマリキーの指定

@ GeneratedValue(strategy = GenerationType.AUTO)

 プライマリキーのフィールドに対して値を自動生成する。strategyに生成方法を指定し、サンプルではGenerationType.AUTOと列挙型の値を自動的に割り振るように指定している。

@ Column

 フィールドに割り当てられるカラム名を指定。nameを省略した場合、フィールド名がそのままカラム名と利用される。

@ Colomnの引数 説明
name カラム名の指定
length 最大の長さ(Stringでは文字数)を指定
nullable nullを許可するかどうかの指定

リポジトリについて

エンティティ -> テーブルに保管されるデータをJava内でオブジェクトとして扱えるようにするためのクラス
リポジトリ -> インターフェースとして用意、汎用的なDBアクセスの処理を自動的に生成して実装するのでほとんどコードを書く必要がない。

JpaRepositoryを継承して作成される

@ Repository

 このクラスがデータアクセスのクラスであることを示す。

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

}

@ Autowired

サンプルでは、@AutowiredでMyDataRepositoryインスタンスをフィールドにインジェクションしている。
インスタンス変数の前に@Autowiredをつけると、@Componentのついたクラスの中から該当するものを探し、newしてフィールドに突っ込んでくれる。@Componentとあるがレイヤによって、@Controller,@Repository,@Serviceが使い分けられる。

findAllメソッド

継承元である、JpaRepositoryに用意されているメソッドでこれによってエンティティが全て自動的に取り出すことができる。

@Controller
public class SampleController {

  @Autowired
  MyDataRepository repository;

  @RequestMapping("/")
  public ModelAndView home(ModelAndView mav) {
    mav.setViewName("home");
    Iterable<MyData> list = repository.findAll();
    mav.addObject("data", list);
    return mav;
  }
}

エンティティのCRUD

 「Create」「Read」「Update」「Delete」

@ ModelAttribute

 エンティティクラスのインスタンスを自動的に用意するのに用いられる。引数には、インスタンスの名前を指定。
  GETアクセスの際のindexメソッドでは、MyDataの引数にはnewされたインスタンスが作成されて、割り当てられる(値などは全て初期値)
 POSTアクセスで呼ばれるformメソッドでは、送信されたフォームの値が自動的にMyDataインスタンスにまとめられて渡される。

SaveAndFlushによる保存

repository.saveAndFlush(mydata);

用意されたエンティティは、JpaRepositoryに用意されているsaveAndFlushメソッドの引数に指定されて、永続化される。

@ Transactional

@Transactionalアノテーションは、トランザクション機能のためのもので、これをメソッドにつけることで、メソッド内で実行されるDBアクセスが一括して実行されるようになる。
 データの書き換えを行うような処理の場合に、途中で外部からDBがアクセスされてデータの整合性に問題を生じさせないようにするなどトランザクションは重要な処理。

引数のreadOnly=falseは文字通り、「読み込みのみ(書き換え不可)」であることを示す。
readOnly=falseとすると読み込みのみのトランザクションから保存などデータの更新を許可するトランザクションに変わる。

SampleController.java
@Controller
public class SampleController {
  @Autowired
  MyDataRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public ModelAndView index(@ModelAttribute("formModel") MyData mydata,
      ModelAndView mav) {
    mav.setViewName("index");
    mav.addObject("msg", "this is sample content.");
    Iterable<MyData> list = repository.findAll();
    mav.addObject("datalist", list);
  }

  @RequestMapping(value = "/", method = RequestMethod.POST)
  @Transactional(readonly = false)
  public ModelAndView form(@ModelAttribute("formModel") MyData mydata,
      ModelAndView mav){
    repository.saveAndFlush(mydata);
    return new ModelAndView("redirect:/");        
  }
}
<body>
  <table>
  <form method="post" action="/" th:object="${formModel}">
    <tr>
      <td><label for="name>名前</label></td>
      <td><input type="text" name="name" th:value="*{name}" /></td>
    </tr>
    <tr>
      <td><label for="name>年齢</label></td>
      <td><input type="text" name="age" th:value="*{age}" /></td>
    </tr>
    <tr>
      <td><label for="name>メール</label></td>
      <td><input type="text" name="mail" th:value="*{mail}" /></td>
    </tr>
    <tr>
      <td><label for="name>メモ</label></td>
      <td><textarea name="memo" th:value="*{memo}" cols="20" rows="5"></textarea></td>
    </tr>
  </form>
  </table>
  <hr/>
  <table>
    <tr>
      <th>ID</th>
      <th>名前</th>
    </tr>
    <tr th:each="obj : ${datalist}">
      <td>${obj.id}</td>
      <td>${obj.name}</td>
    </tr>
  <table>
</body>

@ PostConstruct

 コンストラクタによりインスタンスが生成された後に呼び出されるメソッドであることを示す。コントローラーは最初に一度だけインスタンスが作成され、以降はそのインスタンスが保持される。

SampleComtroller.java
@PostConstruct
public void init(){
    MyData d1 = new MyData();
    d1.setName("tuyano");
    d1.setAge(123);
    d1.setMail("syoda@tuyano.com");
    d1.setMemo("this is my data!");
    repository.saveAndFlush(d1);
    MyData d2 = new MyData();
    d2.setName("hanako");
    d2.setAge(15);
    d2.setMail("hanako@flower");
    d2.setMemo("my girl friend.");
    repository.saveAndFlush(d2);
    MyData d3 = new MyData();
    d3.setName("sachiko");
    d3.setAge(37);
    d3.setMail("sachico@happy");
    d3.setMemo("my work friend...");
    repository.saveAndFlush(d3);
}

Dataの更新

【テンプレート】
<form>にはth:object="${formModel}"が指定されているが、このformModelに編集するエンティティが設定されていれば、type="hidden"によってそのIDが非表示フィールドに格納される。

<body>
    <h1 th:text="${title}">Edit page</h1>
    <table>
    <form method="post" action="/edit" th:object="${formModel}">
        <input type="hidden" name="id" th:value="*{id}" />
        <tr><td><label for="name">名前</label></td>
            <td><input type="text" name="name" th:value="*{name}" /></td></tr>
        <tr><td><label for="age">年齢</label></td>
            <td><input type="text" name="age"  th:value="*{age}" /></td></tr>
        <tr><td><label for="mail">メール</label></td>
            <td><input type="text" name="mail"  th:value="*{mail}" /></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></td><td><input type="submit" /></td></tr>
    </form>
    </table>
</body>

 IDでエンティティを検索して取り出す処理をリポジトリに実装する。
findByIdはID番号を引数にして、MyDataインスタンスを取り出すメソッド。

MyDataRepository.java
@Repository
public interface MyDataRepository extends JpaRepository<MyData, Long> {
    public MyData findById(Long name):
}

 GETアクセス時に呼ばれるeditメソッドでは、クエリで送られたIDを引数とするfindByIdによってエンティティを取得して、そのgetを使い、formModelと名前を指定してaddObjectしている。
 フォームが/editに送信されるとupdateメソッドが呼び出される。送られたフォームのデータを元にエンティティの保存を行う際にも、新たにデータを保存すりのと同じsaveAndFlushで更新をする。新規保存と更新の違いは、引数のエンティティにIDが指定されているかどうか。

SampleController.java
@RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
public ModelAndView edit(@ModelAttribute MyData mydata, @PathVariable int id, ModelAndView mav) {
    mav.setViewName("edit");
    mav.addObject("title","edit mydata.");
    MyData data = repository.findById((long)id);
    mav.addObject("formModel",data.get());
    return mav;
}
@RequestMapping(value = "/edit", method = RequestMethod.POST)
@Transactional(readOnly=false)
public ModelAndView update(@ModelAttribute MyData mydata, ModelAndView mav) {
    repository.saveAndFlush(mydata);
    return new ModelAndView("redirect:/");
}

エンティティの削除

 /deleteにGETアクセスした際のdeleteメソッドと、POSTアクセスを処理するremoveメソッドを用意している。
 deleteメソッドでは指定IDのエンティティをfindByIdで検索しt表示させる。その後POST送信されたremoveメソッドで送られたエンティティの削除を行う。deleteもDBの変更を伴うのでメソッドに@Transactionalアノテーションをつけることを忘れない。

@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)
public ModelAndView delete(@PathVariable int id, ModelAndView mav) {
  mav.setViewName("delete");
  mav.addObject("title", "delete mydata.");
  MyData data = repository.findById((long)id);
  mav.addObject("formModel", data);
  return mav;
}
@RequestMapping(value = "/delete", method = RequestMethod.POST)
@Transactional(readOnly = false)
public ModelAndView remove(@RequestParam long id, ModelAndView mav){
  repository.delete(id);
  return new ModelAndView("redirect:/");
}

リポジトリのメソッド自動生成

JpaRepositoryには辞書によるコードの自動生成機能が組み込まれている。JpaRepositoryは、JPQLによるクエリテキストの生成と実行が行われている。

findById(引数)
   ↓
"find" "by" "id" 引数
   ↓
"select * from テーブル where id = 引数"というクエリの実行

JPQLの使い方

And

 2つの項目の値の両方に合致する要素を検索するような場合に用いられる。
それぞれの項目の値として引数を2つ用意する。

findByIdAndName
   ↓
from MyDataEntity where id = ?1 and name = ?2

Or

 2つの項目の値のどちらか一方に合致する要素を検索するような場合に用いられる。
それぞれの項目の値として引数を2つ用意する。

findByIdOrName
    ↓
from MyDataEntity where id = ?1 or name = ?2

Between

 2つの引数を値で渡し、両者の間の値を検索するような時に用いられる。指定の項目が一定範囲内の要素を検索する。

findByAgeBetween
   ↓
from MyDataEntity where age between ?1 and ?2

LessThan

 数値の項目で、引数に指定した値より小さいものを検索する。

findByAgeLessThan
   ↓
from MyDataEntity where age < ?1

GreaterThan

数値の項目で引数に指定した値より大きいものを検索する。

findByAgeGreaterThan
   ↓
from MyDataEntity where age > ?1

IsNull

 指定の項目の値がnullのものを検索する。

findCommentIsNull
   ↓
from MyDataEntity where comment is null

IsNotNull, NotNull

 指定の項目の値がnullでないものを検索する。NotNullでも理解する

findByCommentNotNull,
findByCommentIsNotNull
   ↓
from MyDataEntity where comment not null

Like

 テキストのLIKE検索を行う。指定の項目から値を曖昧に検索する。引数に渡す値にワイルドカードをつけて使用することができる。

findByNameLike
   ↓
from MyDataEntity where name like ?1

NotLike

 検索文字列をふくまないものを曖昧に検索。引数に渡す値にワイルドカードをつけて使用することができる。

findByNameNotLike
   ↓
from MyDataEntity where name not like ?1

OrderBy

 並び順の指定。通常の検索メソッド名の後につけられる。項目名の後にAscDescをつけることで昇順か降順かを指定できる。

findByNameOrderByAgeAsc
   ↓
from MyDataEntity where name = ?1 order by age Asc

Not

 指定の項目が引数の値と等しくないものを検索する。

findByNameNot
   ↓
from MyDataEntity where name <> ?1

In

 指定の項目の値が、引数のコレクションに用意された値のどれかと一致すれば検索する

findByNameIn(Collection<String> name)
   ↓
from MyDataEntity where name in ?1

NotIn

 指定の項目の値が引数のコレクションに用意されたどの値とも一致しないものを検索する

findByNameNotIn(Collection<String> name)
   ↓
from MyDataEntity where name not in ?1

JpaRepositoryのメソッド実装例

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

    // "%" + str + "%"のようにワイルドカード指定必要
    public List<MyData> findById(Long name);
    public List<MyData> findByNameLike(String name);
    public List<MyData> findByIdIsNotNullOrderByIdDesc();
    public List<MyData> findByAgeGreaterThan(Integer age);
    public List<MyData> findByAgeBetween(Integer age1, Integer age2);

}
  • メソッド名はキャメル記法
  • メソッド名の単語の並び重要find[By○○][その他の検索条件][OrderByなど]
  • 引数の型が一致しているか確認する

単純な検索は全て自動生成メソッドで。複雑な検索処理だけ、DAOで定義して利用する

エンティティのバリデーション

 バリデーションをモデルに予め用意しておくことで、入力される各項目にルールが設定され、入力値が違反していないかチェックできる。

@ Validated

 これがエンティティの値をバリエーションチェックする。これをつけることで、エンティティの各値を自動的にチェックするようになる。

@ BindingResult

 バリデーションチェックの結果は、その次のBindingResultという引数で受け取る。
エラーが発生しているかどうかは、hasErrorsメソッドで調べられる。trueならばエラーあり、falseならエラーなしなので、下の場合、if(!result.hasErrors())の中でエラーがない場合の処理(データの保管など)をしていく。

if(!result.hasErrors()) {...}

エラーメッセージの出力

 th:eachに${#fields.detailedErrors()}に変数式が設定されているのは、#fieldsにエンティティの各フィールドをバリデーションチェックした結果などがまとめられたオブジェクトで、detailedErrorsメソッドは、発生したエラーに関する情報をひとまとめのリストとして返すもの。th:eachでは、リストから順にエラーのオブジェクトを取り出して変数errorに設定する。

<li th:each="error : ${#fields.detailedErrors}" class="err" th:text="${error.message}">

errorにはmessageというプロパティがあり、これで発生したエラーのメッセージが得られる。th:text="${error.message}"という形で出力される。

【テンプレート】

<form method="post" action="/" th:object="${formModel}">
    <ul>
        <li th:each="error : ${#fields.detailedErrors()}"
            class="err" th:text="${error.message}" />
    </ul>
    <tr><td><label for="name">名前</label></td>
        <td><input type="text" name="name" 
            th:field="*{name}" /></td></tr>
    <tr><td><label for="age">年齢</label></td>
        <td><input type="text" name="age"
            th:field="*{age}" /></td></tr>
    <tr><td><label for="mail">メール</label></td>
        <td><input type="text" name="mail" 
                th:field="*{mail}" /></td></tr>
    <tr><td><label for="memo">メモ</label></td>
        <td><textarea name="memo" th:field="*{memo}" 
            cols="20" rows="5" ></textarea></td></tr>
    <tr><td></td><td><input type="submit" /></td></tr>
</form>

【エンティティ】

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column
    @NotNull
    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;

    // ……アクセサは省略……
}

【コントローラー】
 エンティティで設定したバリデーションに入力値が違反していないかチェックし、ルールを満たしている場合のみ値の保管を行えるようにするサンプル

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(
    @ModelAttribute("formModel") MyData mydata, 
        ModelAndView mav) {
    mav.setViewName("index");
    mav.addObject("msg","this is sample content.");
    mav.addObject("formModel",mydata);
    Iterable<MyData> list = repository.findAll();
    mav.addObject("datalist",list);
    return mav;
}

@RequestMapping(value = "/", method = RequestMethod.POST)
@Transactional(readOnly=false)
public ModelAndView form(
    @ModelAttribute("formModel") @Validated MyData mydata, 
    BindingResult result,
    ModelAndView mov) {

    ModelAndView res = null;
    if (!result.hasErrors()){
        repository.saveAndFlush(mydata);
        res = new ModelAndView("redirect:/");
    } else {
        mov.setViewName("index");
        mov.addObject("msg","sorry, error is occured...");
        Iterable<MyData> list = repository.findAll();
        mov.addObject("datalist",list);
        res = mov;
    }
    return res;
}

各入力フィールドにエラーを表示

<input type="text" name="name" th:value="*{name}" th:errorclass="err" />

 th:value="*{name}"としてオブジェクトのnameプロパティを値に設定している。th:errorclassという属性が用意されていて、エラーが発生した際に適用されるクラス名を指定する。これによってエラーが発生したときのみの処理を適用している。

<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" th:errorclass="err">

hasErrorsは引数に指定したフィールドにエラーが発生しているかどうかをth:ifを使ってtrueの時だけダグを表示させている。

エラーメッセージの表示はth:errorsという属性を利用し、サンプルでは、*{name}を指定している。

【テンプレート】

<form method="post" action="/" 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:errors="*{name}" th:errorclass="err">
                </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:errors="*{age}" th:errorclass="err">
                </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:errors="*{mail}" th:errorclass="err">
                </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></td><td><input type="submit" /></td></tr>
    </form>

javax.validationによるアノテーション

バリデーションアノテーション 内容
@ Null 値がnullであることのチェック
@ NotNull 値がnullであることを認めない
@ Min(int num) 数値(整数)を入力する項目で入力可能な最小値を指定する
@ Max(int num) 数値(整数)を入力する項目で入力可能で最大値を指定する
@ DecimalMin(String num) BigDecimal,BigInteger,String値で値を設定する場合の最小値の指定 intもOK
@ DecimalMax(String num) BigDecimal,BigInteger,String値で値を設定する場合の最大値の指定 intもOK
@ Degits(integer=5, fraction=10) 整数部分と小数部分の桁数制限
@ Future 現在より未来の日時のみを受け付けるようにする
@ Past 現在より過去の道時のみを受け付けるようにする
@ Size(min=1, max=10) Stringのほか、配列、コレクションクラスなどのオブジェクトの保管される要素数の指定
@ Pattern(regexp="[a-zA-Z]+") 正規表現のパターンを指定して入力チェックを行う

HiberinateValidatorによるアノテーション

バリデーションアノテーション 内容
@ NotEmpty nullも空文字も認めない
@ Length(min=5, max=10) Stringtに使用される文字列の長さの範囲を指定するアノテーション
@ Range 最小値、最大値をの範囲指定をするために数値項目で使用される。@ Minと@ Maxをまとめられる
@ Email 入力された値が電子メールアドレスかどうかをチェックする
@ CreditCardNumber クレカ番号形式かどうかのチェックをするために数値かString値で数値で入力される項目で用いる。
@ EAN バーコード識別番号の規格

エラーメッセージについて

英語で表示されるエラーメッセージをカスタマイズする方法
エンティティクラスでバリデーションのためアノテーションを用意する際にmessageという値を使って表示メッセージを指定する

@NotEmpty(message="空白は不可")

@Email(message="メールアドレスのみ")

@Min(value=0, message="ゼロ以上")
@Max(value=200, message="200以下")

プロパティファイルの利用

実際の開発ではメンテナンス性が悪いのでメッセージは、プロパティファイルを用意して管理すべき
resourceフォルダに「ValidationMessages.properties」という名前でテキストファイルを作成することで対応できる

オリジナルのバリデータを作成する

バリデータの自作には

  • アノテーションクラス
  • バリデータクラス が必要

アノテーションクラスは、
 @ interfaceの後に名前を記述するアノテーション型のクラスを作成
バリデータクラスは、javax.validation.ConstraintValidatorインターフェースを実装して定義する。ConstraintValidatorにはinitializeisValidの2つのメソッドがあり、これらを実装して必要な処理を記述する。

  • intializeメソッド

初期化メソッド
引数には総称型で指定したアノテーションクラスが渡される
必要に応じてアノテーションに関する情報を取得する

  • isValidメソッド

バリデーション処理を行う部分
引数には入力された値(String)とConstraintValidatorインスタンスが渡される。
ここで値をチェックしてバリデーションの正否に応じてboolean値を返す。

【アノテーションクラス】

@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@ReportAsSingleViolation
public @interface Phone {
  String message() default "Please input a phone number.";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() dedault {};
}

【バリデーションクラス】

public class PhoneValidator implements ConstraintValidator<Phone, String> {

  @Override
  public void initialize(Phone phone){
  }

  @Override
  public boolean isValid(String input, ConstraintValidatorContext cxt){
    if(input == null){
      return false;
    }
    return input.matches("[0-9()-]*");
  }

}

6. データベースアクセスを掘り下げる

 リポジトリを用意することで、ほぼコードを書くことなくDBアクセスが可能であるが、限界があるので、SpringDataJPAに用意されているDBアクセスの機能をSpringBootアプリケーション内から利用する方法について考える。

DataAccessObject

 DBを利用する場合、データを表示するページにアクセスしたら、そのコントローラーのリクエストハンドラ内でデータを取り出して、ビューに表示する処理を用意すること。つまり、コントローラーの各リクエストハンドラに、必要なDBアクセスの処理を用意する。

リクエストごとに処理を書くとコントローラーが肥大化するので、データアクセスとロジックは分離させる。よってDBアクセスする手段を提供するDAO(DataAccessObject)というオブジェクトを用意する。これらをコントローラーから呼び出して必要な処理を行う。

EntityManagerクラスを保管するためのフィールドを用意することで、エンティティを利用するために必要な機能を提供する。

createQueryによるクエリの作成

 QueryはSQLでデータを問い合わせるためのクエリ一文に相当する機能を持つオブジェクト
 JPQLによるクエリとなるのがQueryインスタンス
 EntityManagerのcreateQueryは引数にJPQLによるクエリ一文を指定して呼び出すとクエリを実行するためのQueryインスタンスが生成される。
サンプルのfrom MyDataとはselect * from mydataに相当するJPQLのクエリ一文

Query query = entityManager.createQuery("from MyData");

Queryから結果を取得する

 作成されたQueryがもつgetResultListメソッドにより、クエリに実行結果をインスタンスとして取得できる。

List<MyData> list = query.getResultList();

@ SuppressWarnings

サンプルではlist変数に付けられているアノテーションでコンパイル時の警告を抑制する。uncheckedを引数にすることで、メソッドの戻り値が格納先の変数の型の値として得られているかチェックを行う。動作そのものには関係がなく、ビルド時に警告が出ないようにするもの

@SuppressWarnings("unchecked")

【DAOインターフェース】

public interface MyDataDao <T> extends Serializable {   
    public List<T> getAll();    
}

【DAOの実装クラス】

@Repository
public class MyDataDaoImpl implements MyDataDao<MyData> {
    private static final long serialVersionUID = 1L;

    private EntityManager entityManager;

    public MyDataDaoImpl(){
        super();
    }
    public MyDataDaoImpl(EntityManager manager){
        this();
        entityManager = manager;
    }

    @Override
    public List<MyData> getAll() {
        Query query = entityManager.createQuery("from MyData");
        @SuppressWarnings("unchecked")
        List<MyData> list = query.getResultList();
        entityManager.close();
        return list;
    }

}

コントローラーの実装

 DAOでエンティティを利用するコントローラーを作成する。

@ PersistenceContext

 @PersistenceContextは、EntityManagerのBeanを取得してフィールドに設定する。
 SpringBootではEntityManagerが自動的にBeanとしてインスタンスが登録されているので、これを@PersistenceContextでコントローラーのフィールドに割り当てる。
 @PersistenceContextを使ったBeanのバインドは、「1クラスにつき1インスタンス」までしか置けない

@PersistenceContext
EntityManager entityManager;

 DAO経由でエンティティからリストを取得してビューのテンプレート名の設定と同時にテンプレート側にリストのオブジェクトを渡している。DAOにエンティティへのアクセスの処理をまとめておけば、コントローラー側で簡単にアクセス処理が呼び出せる。

Iterable<MyData> list = dao.getAll();
mav.addObject("datalist", list);
public class SampleController {
  // リポジトリ
  @Autowired
  MyDataRepository repository;

  // エンティティ
  @PersistenceContext
  EntityManager entityManager;

  // DAO
  MyDataDaoImpl dao;

  @PostConstruct
  public void init() {
    dao = new MyDataDaoImpl(entityManager);
    MyData md = new MyData():
    md.setHoge("fuga");
    md.setNumber(123);
    repository.saveAndFlush(md);
  }

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public ModelAndView index() {
    mav.setViewName("index");
    mav.addObject("msg", "MyDataのサンプルです。");
    Iterable<MyData> list = dao.getAll();
    mav.addObject("datalist", list);
    return mav;
  }
}

DAOに検索メソッドを追加する

【DAOインターフェース】

public interface MyDataDao<T> extends Serializable {

    public List<T> getAll();
    // IDを引数にしてエンティティを検索して返す
    public T findById(long id);
    // 名前からエンティティを検索して返す
    public List<T> findByName(String name);
}

【DAO実装クラス】

"from MyData where id = " -> JPQLのクエリ(idを引数にしてエンティティを取得)
getSingleResultQueryから得られるエンティティを1つだけ取り出して返すもの
1つのエンティティしか検索されないようなもの(IDとか)については、Listで返すより、得られたエンティティをそのまま返した方が良い

@Override
public MyData findById(long id) {
    return (MyData)entityManager.createQuery("from MyData where id = " 
        + id).getSingleResult();
}

"from MyData where name = " -> JPQLのクエリ(名前を引数にしてエンティティを取得)
getResultListはListを返すのでfindByNameList<MyData>が戻り値の型

@SuppressWarnings("unchecked")
@Override
public List<MyData> findByName(String name) {
    return (List<MyData>)entityManager.createQuery("from MyData where name = " 
        + name).getResultList();
}

JPQLを利用する

JPQLは、SQLのクエリと似たクエリ文を実行することでJPAがSQLクエリを生成し、DBを操作する簡易言語

HttpServletRequest

 フォームをPOSTで受け取るメソッドなど、送信された値を受け取って処理を行うときには@RequestParamを引数に使ってきたが、HttpServletRequestを引数に使うこともできる。
 実は、@RequestParamのパラメータはHttpServletRequestgetParameterを呼び出してパラメータを受け取る操作を自動的に行って、その結果を引数に設定するもの
 HttpServletResponseも引数に指定することができる

@RequestMapping(value = "/find", method = RequestMethod.POST)
public ModelAndView search(HttpServletRequest request,
        ModelAndView mav) {
    mav.setViewName("find");
    String param = request.getParameter("fstr");
    if (param == ""){
        mav = new ModelAndView("redirect:/find");
    } else {
        mav.addObject("title","Find result");
        mav.addObject("msg","「" + param + "」の検索結果");
        mav.addObject("value",param);
        List<MyData> list = dao.find(param);
        mav.addObject("datalist", list);
    }
    return mav;
}

DAOへのfindメソッドの追加(名前付きパラメータを使う)

クエリ「"from MyData where id = :fstr"」の:fstrのような形式でJPQLのクエリ文に書かれたものはパラメータ用の変数として扱われる。
 その後に続く、QueryインスタンスのsetParameterが、第1引数の変数に第2引数の値を設定して直前の:fstrに値を代入してクエリを作る
 
【DAOインターフェース】

public List<T> find(String fstr);

【DAO実装クラス】

@Override
public List<MyData> find(String fstr){
    List<MyData> list = null;
    String qstr = "from MyData where id = :fstr";
    Query query = entityManager.createQuery(qstr)
        .setParameter("fstr", Long.parseLong(fstr));
    list = query.getResultList();
    return list;
}

複数のパラメータをクエリにいくつでも埋め込める

 setParameter(パラメータを設定済みのQueryインスタンスを返す)をメソッドチェーンで連ねて、一つのクエリにパラメータをいくつも設定できる

String qstr = "from MyData where id = :fid or name like :fname or mail like :fmail";
Long fid = 0L;
Query query =
    entityManager.createQuery(qstr).setParameter("fid", fid)
            .setParameter("fname", "%" + fstr + "%")
            .setParameter("fmail", fstr + "@%");

「?」による番号指定のパラメータ

 クエリ内に埋め込むパラメータ用変数は、番号指定によって値を設定するものもある。?1のように、「?」の後に数字を指定をすることでパラメータの埋め込み一を設定する

String qstr = "from MyData where id = ?1 or name like ?2 or mail like ?3";
Long fid = 0L;
    Query query = entityManager.createQuery(qstr).setParameter(1, fid)
        .setParameter(2, "%" + fstr + "%")
        .setParameter(3, fstr + "@%");

クエリアノテーション

 名前付きクエリは@NamedQueryを使って作ることができる。エンティティクラスの宣言の前に付ける
 クエリの文字列に名前をつけて設定する

@Entity
@NamedQuery(
  name = "findWithName",
  query = "from MyData where name like :fname"
)
@Table(name = "mydata")
public class MyData {
  ...
}

複数のクエリをまとめるために@NamedQueriesを使う
カンマで区切っていくらでも@NamedQueryを追加できる

@NamedQueries (
    @NamedQuery(
        name="findWithName",
        query="from MyData where name like :fname"
    )
)

createNamedQueryは引数にクエリアノテーションの名前をテキストで指定することで、その名前のクエリを取得して、Queryインスタンスを作成する
 名前付きクエリを使うことでDAOのコードから切り離して、エンティティクラスにクエリを置く
-> 複数のエンティティを扱うときに同じ働きのクエリを同じ名前でそれぞれに置くことでわかりやすい

Query query = entityManager
    .createQuery("findWithName")
    .setParameter("fname", "%" + fstr + "%");

リポジトリと@ Query

検索機能を拡張するたびにエンティティを書き換えるのはナンセンスなので、実際にDBアクセスを実行する側にクエリを準備したい
 クエリは、リポジトリとなるインターフェースに@Queryで宣言することができる
@Queryはアノテーションを記述したメソッドを呼び出す際に指定されたクエリが使われるようになる

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

    @Query("SELECT d FROM MyData d ORDER BY d.name")
    List<MyData> findAllOrderByName();
}

 コントローラーから、クエリを実行するリポジトリを呼び出してみる
 クエリを用意する場所が、DAOかリポジトリかの違いなので内部的な違いはない

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(ModelAndView mav) {
    mav.setViewName("index");
    mav.addObject("title","Find Page");
    mav.addObject("msg","MyDataのサンプルです。");
    Iterable<MyData> list = repository.findAllOrderByName(); //dao.getAll();
    mav.addObject("datalist", list);
    return mav;
}

@ NamedQueryのパラメータ

 詳細な検索を行いたい場合、クエリの中に検索条件のための値を組み込む必要が生じる
 こうした場合に、クエリアノテーションに設定するクエリテキストにパラメータを用意することができる
 

@NamedQuery (
    name = "findByAge",
    query = "from MyData where age > :min and age < :max"
)

 エンティティクラスの手前にこのような形で@NamedQueryを用意しておいて、クエリに埋め込まれたパラメータに値を渡すようにしてDAOから呼び出す
 DAOから実際にfindByAgeを呼び出してみる
 Queryインスタンスの作成はcreateNamedQueryを引数をクエリの名前を指定して行う
 その後setParameterを使って@NamedQueryに用意したクエリテキストのパラメータに値を渡す
 そして、getResultListでエンティティを検索する

public List<MyData> findByAge(int min, int max);
@suppresswarning
@Override
public List<MyData> findByAge(int min, int max) {
    return (List<MyData>) entityManager
        .createNamedQuery("findByAge")
        .setParameter("min", min)
        .setParameter("max", max)
        .getResultList();
}

@ Queryを使う

 @Queryを使う場合もクエリテキストに変数を埋め込み、メソッドの引数で指定するやり方としては同じ
 ただし、@Paramを使い、どの変数がどのパラメータと関連付けられるかを指定する
 以下@NamedQueryに追加したfindByAgeをリポジトリに用紙する場合のサンプル
 
【リポジトリのインターフェース】

@Query("from MyData where age > :min and age < :max")
public List<MyData> findByAge(@Param("min") int min, @Param("max") int max);

 エンティティ自体にクエリを持たせるか、リポジトリに用意するかはアプリケーションの設計によって最適な方を使い分ける。

Criteria APIによる検索

 JPQLのような言語を使わず、メソッドチェーンによってDBアクセスを行う機能も用意されている -> Criteria API
 Criteria APIは3つのクラスを組み合わせて利用する

クラス できること
CriteriaBuilder クエリ生成の管理
CriteriaQuery クエリ実行
Root 検索されるエンティティのルートでここからエンティティを絞り込んだりする

①CriteriaBuilderの取得
 CriteriaBuilderのインスタンスの用意をして、EntityManager#getCriteriaBuilderを呼び出す

CriteriaBuilder builder = entityManager.getCriteriaBuilder();

②CriteriaQueryの作成
 クエリ文を引数としたりはしない
 特定のエンティティにアクセスするためにそのエンティティのclassプロパティを引数に指定する

CriteriaQuery <Entity> query = builder.createQuery(Entity.class);

③Rootの取得
  RootをCriteriaQueryのfromメソッドで取得
 引数には検索するエンティティのClassプロパティを指定

Root <Entity> root = query.from(Entity.class);

④CriteriaQueryメソッドの実行
CriteriaQueryでエンティティを絞り込むためのメソッドの呼び出し
必要に応じてメソッドチェーンで呼び出す

query.select(root);

⑤createQueryして結果を取得
 最後にcreateQueryでQueryを生成し、getResultListで結果のListを取得
 createQueryの引数にはQriteriaQueryを指定するのが通常のQueryによる検索処理との違い

List<Entity> list = (List<MyData>)entityManager
    .createQuery(query)
    .getResultList();
return list;

以下、全エンティティの取得をgetAllメソッドによって行うサンプル

 Rootの取得にはfromメソッドの引数としてMyData.classを指定
全MyDataかを情報として保持したRootインスタンスが得られる
Root取得後、全てのMyDataを取得するのに、CriteriaQuery#selectを引数にMyData.classを指定して呼び出す
 後は、このCriteriaQuerycreateQueryし、getResultすればすべてのMydataが取得できるようになっている

@Override
public List<MyData> getAll() {
    List<MyData> list = null;       
    CriteriaBuilder builder = 
            entityManager.getCriteriaBuilder();
    CriteriaQuery<MyData> query = 
            builder.createQuery(MyData.class);
    Root<MyData> root = query.from(MyData.class);
    query.select(root);
    list = (List<MyData>)entityManager
            .createQuery(query)
            .getResultList();
    return list;
}

CriteriaAPIによる名前検索

 引数のテキストとnameの値が一致するエンティティだけを検索するようにしている
 Rootインスタンスを取得した後、取り出すエンティティを絞り込むための処理をする
 
select(root)後にメソッドチェーンを使ってwhereメソッドをExpression(様々な式の評価を扱う)を引数としてエンティティを絞り込む処理を行う
  equalメソッドに引数に指定したExpressionとObjectによって両者が等しいかどうかを確認し、結果をPredicateクラスのインスタンスとして返す
 Predicateはメソッドに指定される条件や式をオブジェクトとして表す役割があるので、例えばequalなら引数に指定したものが等しい条件を示すPredicateが用意され、条件に合致するエンティティを絞り込む

// where(Expression<boolen>)
// equal(Expression, Object)
query.select(root)
        .where(builder.equal(root.get("name"), fstr));

CriteriaBuilderのメソッド

メソッド 書式 説明
equal equal(Expression<?> x, Expression<?> y) 引数が等しいことを検証するための述語を作ります。
gt gt(Expression<? extends java.lang.Number> x, Expression<? extends java.lang.Number> y) 最初の引数が二つ目の引数より大きいかどうか検証するための述語を作ります。
greaterThan greaterThan(Expression<? extends Y> x, Expression<? extends Y> y) 最初の引数が二つ目の引数より大きいかどうかを検証するための述語を作ります。
ge ge(Expression<? extends java.lang.Number> x, Expression<? extends java.lang.Number> y) 最初の引数が二つ目の引数以上であるかどうか検証するための述語を作ります。
greaterThanOrEqualTo greaterThanOrEqualTo(Expression<? extends Y> x, Expression<? extends Y> y) 最初の引数が二つ目の引数以上であるかどうか検証するための述語を作ります。
lt lt(Expression<? extends java.lang.Number> x, Expression<? extends java.lang.Number> y) 最初の引数が二つ目の引数未満であるかどうか検証するための述語を作ります。
lessThan Predicate lessThan(Expression<? extends Y> x, Y y)
le le(Expression<? extends java.lang.Number> x, java.lang.Number y) 最初の引数が二つ目の引数以下であるかどうか検証するための述語を作ります。
lessThanOrEqualTo lessThanOrEqualTo(Expression<? extends Y> x, Y y) 最初の引数が二つ目の引数以下であるかどうか検証するための述語を作ります。
between between(Expression<? extends Y> v, Expression<? extends Y> x, Expression<? extends Y> y) 最初の引数が二つ目と三つ目の引数の間の値であるか検証するための述語を作ります。
isNull isNull(Expression<?> x) 式がnullかどうかを検査する述語を作ります。
isNotNull isNotNull(Expression<?> x) 式がnullでないかどうかを検査する述語を作ります。
isEmpty isEmpty(Expression collection) コレクションが空かどうかを検証するための述語を作ります。
isNotEmpty isNotEmpty(Expression collection) コレクションが空でないかどうかを検証するための述語を作ります。
like like(Expression x, Expression pattern) 式が与えられたパターンを満たすかどうかを検証する述語を作ります。
and and(Expression x, Expression y) 与えられたブール式の論理積を作ります。
or or(Expression x, Expression y) 与えられたブール式の論理和を作ります。
not not(Expression restriction) 与えられた限定の否定を作ります。

orderByによるエンティティのソート

 検索された結果はエンティティをListとして取得する際にorderByメソッドを使用する
 引数のExpressionCriteriaBuilder#getを使ってエンティティのプロパティを示すPathを指定する
 サンプルのorderByの引数はnameの要素について昇順に並べ替えるPredicateを設定している

query.select(root).orderBy(builder.asc(root.get("name")));

取得位置と取得個数の設定

 Queryのエンティティ取得メソッドは、エンティティを1つだけ返すgetSingleResultと全エンティティをListで返すgetResultListがある
 実際のDB利用の際には、位置と個数を指定して取得することもできる

  • 指定の位置から取得する

 引数に整数値を指定する
 取得位置はリストの先頭を0とする

 query.setFirstResult(int pos);
  • 指定の個数を取得する

 取得する個数を指定
 設定されるのは得られる最大数なのでエンティティが足りない場合はあるだけが取り出される

@Override
public List<MyData> getAll() {
    int offset = 1; // 取り出す位置の指定
    int limit = 2;  // 取り出す個数の指定
    List<MyData> list = null;
    CriteriaBuilder builder = 
            entityManager.getCriteriaBuilder();
    CriteriaQuery<MyData> query = 
            builder.createQuery(MyData.class);
    Root<MyData> root = 
            query.from(MyData.class);
    query.select(root);
    list = (List<MyData>)entityManager
            .createQuery(query)
            .setFirstResult(offset)
            .setMaxResults(limit)
            .getResultList();
    return list;
}

エンティティの連携

 複数のテーブルが関連して動くようなDBは「アソシエーション」と呼ばれる機能によりエンティティ同士を連携して処理する
-> クラスのプロパティとして別のエンティティを持たせる

@ OneToOne

 2つのエンティティが1対1で対応する連携を示すもの

@ OneToMany

 1つのエンティティに対してもう一方のエンティティの複数が対応する

@ ManyToOne

 複数のエンティティに対し、もう一方のエンティティの1つだけが対応する

@ ManyToMany

 複数のエンティティに対し、他方の複数のエンティティが対応する関係

サンプルのMsgDataは一人のメンバーがいくつでもメッセージを投稿できるので、
@ManyToOneMyDataに関連付けられている

【関連付け元のエンティティ】

@Entity
@Table(name = "msgdata")
@Getter
@Setter
public class MsgData {
  @Id
  @Column
  @NotNull
  private long id;

  @Column
  private String title;

  @Column(nullable = false)
  @NotEmpty
  private String message;

  @ManyToOne
  private MyData mydata;

  //以下、コンストラクタ、アクセサの省略
}

【関連付け先のエンティティ】
 MsgDataと関連付けるためのmsgdatasとうプロパティを追加する

@Entity
@Table(name = "mydata")
@Getter
@Setter
public class MyData {
  @OneToMany(cascade = CascadeType.ALL)
  @Column(nullable = true)
  private List<MsgData> msgdatas;
}

 リポジトリのsaveでは、保存をする際、他のエンティティに関連付けられたフィールドがある場合は、送信された情報を元にインスタンスを取得して自動的に設定する
そのため、関連付いたレコードの登録処理をエンティティの連携を意識してプログラマが実装する必要がない

repository.saveAndFlush(msgdata);

<エンティティ連携のポイント>

  • モデルできちんとアノテーションを付ける
  • フォームに正しく関連付けたエンティティの項目を用意する

7. SpringBootを更に活用する

サービスとは

 ビジネスロジックの中で、アプリケーションからりようできるようコンポーネント化された部分は「サービス層」と一般に呼ばれ、コントローラーやモデルを利用するDAOなどとも違って、どこからでも呼び出して利用できるクラス

コントローラーとビジネスロジック(モデル)の両者から自由に呼び出せるものがサービス

SpringFrameworkはこのサービスをBeanとして登録してアノテーションを記述するだけでいつでも利用できるようになっている

○アプリケーション層
  ・コントローラー
    リクエストハンドラ
  ・モデル
    ビジネスロジック
    エンティティ
○ドメイン層
  ・サービス
  ・リポジトリ

@ Service

 このクラスをサービスとして登録するためのアノテーションで、サービスのクラスはクラス名の前にこのアノテーションをつける

@ PersisitenceContext

 EntityManagerのBeanを自動的に割り当てるためのもので、コントローラークラス側にではなく、サービスにEntityManagerを用意して利用することもできる

@Service
public class MyDataService {

    @PersistenceContext
    private EntityManager entityManager;

    @SuppressWarnings("unchecked")
    public List<MyData> getAll() {
        return (List<MyData>) entityManager
            .createQuery("from MyData").getResultList();
    }

    public MyData get(int num) {
        return (MyData)entityManager
                .createQuery("from MyData where id = " + num)
                .getSingleResult();
    }

    public List<MyData> find(String fstr) {
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<MyData> query = builder.createQuery(MyData.class);
        Root<MyData> root = query.from(MyData.class);
        query.select(root).where(builder.equal(root.get("name"), fstr));
        List<MyData> list = null;
        list = (List<MyData>) entityManager.createQuery(query).getResultList();
        return list;
    }

}

コントローラーでサービスBeanを使う

 DAOからサービスBeanを使ってDBアクセスする
 作成したサービスは@AutowiredでBeanとしてコントローラーに関連付けて使用する

@Serviceが記述されているサービスクラスはアプリケーション内でBean化され、それが@Autowiredによってフィールドに割り当てられる

@Controller
public class SampleController  {

    @Autowired
    MyDataRepository repository;

        // サービスBeanをフィールドに関連付ける 
    @Autowired
    private MyDataService service;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView index(ModelAndView mav) {
        mav.setViewName("index");
        mav.addObject("title","Find Page");
        mav.addObject("msg","MyDataのサンプルです。");
                // 全エンティティの取得
        List<MyData> list = service.getAll(); 
        mav.addObject("datalist", list);
        return mav;
    }

        // "/find"のGETリクエストハンドラ省略

        @RequestMapping(value = "/find", method = RequestMethod.POST)
        public ModelAndView search(HttpServletRequest request,
            ModelAndView mav) {
        mav.setViewName("find");
        String param = request.getParameter("fstr");
        if(param == "") {
            mav = new ModelAndView("redirect:/find");
        } else {
            mav.addObject("title", "Find result");
            mav.addObject("msg", "「" + param + "」の検索結果");
            mav.addObject("value", param);
            // エンティティの検索
            List<MyData> list = service.find(param);
            mav.addObject("datalist", list);
        }
        return mav;
    }

    // 以下コンストラクタ、アクセサ等省略

}

RestControllerを作成する

Webアプリケーション開発における「サービス」とは一般に「外部からアクセスして必要な情報を受け取ることのできるWebプログラム」を意味する
 外部から利用できるサービスとしてJSON形式、XML形式のRESTサービスがある

@RestController
public class MyDataRestController {

    @Autowired
    private MyDataService service;

    @RequestMapping("/rest")
    public List<MyData> restAll() {
        return service.getAll();
    }

    @RequestMapping("/rest/{num}")
    public MyData restBy(@PathVariable int num) {
        return service.get(num);
    }
}

XMLでデータを取得する

 RestControllerでXMLで扱う場合はデータ形式を解析して処理するライブラリ「Jackson DataFormat XML」を用意する

@ XmlRootElement

XMLデータでのルートとなるエレメントであることを示す。リクエストハンドラのrestAll
restByが得られたオブジェクトを返して、通常ならJSON形式に変換され、@XmlRootElementがついていたらXML形式に変換されるだけの違い
Mavenを利用している場合

pom.xml
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

コンポーネントとBean

 コンポーネントはアプリケーション内に自動生成されるBeanで、Beanのインスタンスは@Autowiredでバインドすると利用できるようになる
 サービスはコンポーネントの一種 -> サービス層のコンポーネントがサービスと呼ばれるもの

@ Component

アノテーションによってこれがつけられているクラスがコンポーネントとしてアプリケーションに認識されるようになる
 クラスのインスタンスがBeanとして登録されるようになる

@ Autowiredコンストラクタ

 コンストラクタに@Autowiredがつけられている場合、そのクラスのインスタンスがBeanとして登録される際にこの@Autowiredが指定されたコンストラクタによってインスタンスが生成されるようになる
 @Autowiredのついたコンストラクタがコンポーネントクラスに用意されていないと、アプリケーション実行時にエラーが発生し、起動に失敗するので必ず用意する

 MySampleBean(ApplicationArguments args)のインスタンス引数は、アプリケーションが実行されたとき、つまり@SpringBootApplicationが指定されたクラスでSpringApplication.runが実行されるときに渡された引数を管理するオブジェクト
 実行時に渡される引数を利用する場合はこのインスタンスを引数として用意する
 サンプルのメソッド内ではApplicationArgments#getNonOptionArgsでアプリケーション実行時の引数をListとして取り出す

@Component
public class MySampleBean {

    private int max = 10;

    @Autowired
    public MySampleBean(ApplicationArguments args) {
        List<String> files = args.getNonOptionArgs();
        try {
            max = Integer.parseInt(files.get(0));
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }

        public int count() {
        counter++;
        counter = counter > max ? 0 : counter;
        return counter;
    }
}

コンポーネントの利用

 コンポーネントを利用するためにコントローラークラスにリクエストハンドラを追加する
 @Autowiredでフィールドに関連付けておくと、自動的にバインドされるようになるので、リクエストハンドラ内のbean.countが実行できる
 コンポーネントは@Autowiredでフィールドに関連付けることでどこからでも自由に利用できるようになる
 いくつものリクエストハンドラから呼び出す汎用的な機能があればとりあえずまとめておくとよい

@Autowired
MySampleBean bean;

@RequestMapping("/count")
public int count() {
    return bean.count();
}

また@Controllerはコントローラーの役割を果たすコンポーネント、@Repositoryは、データアクセスのためのコンポーネントとそれぞれ特別な役割が与えられたコンポーネントの一種で、@Conponentはそういった特別な役割を持たないものにつけられる

覚えておきたいその他の機能

 Springにおいては標準で用意されるBeanを@Autowiredなどのアノテーションをつかってバインドすることや自分でコンポーネントサービスを作ってBeanとして利用できるが、
構成クラスを利用してBeanをコントローラーなど各所で利用できる
 実際は構成ファイルからアノテーションへというSpringBootの思想上、構成ファイルを作るというやり方はあまり推奨されない

構成クラスの作成

 特に特殊なクラスを継承していない一般的なクラスでも@Configurationをつけておくと、アプリケーション起動時にこのクラスが構成クラスとしてインスタンス化されてそこに記述されているBeanなどをアプリケーションに登録する

【フィールドにBeanを登録した構成クラス】

@Configuration
public class MyBootAppConfig {

    @Bean
    MyDataBean myDataBean(){
        return new MyDataBean();
    }
}

構成クラス内に@Beanをつけてクラスのインスタンスを返すメソッドを用意すれば、それがBeanとして登録されるようになる

【もととなったBeanクラス】

public class MyDataBean {

    @Autowired
    MyDataRepository repository;

    public String getTableTagById(Long id){
        Optional<MyData> opt = repository.findById(id);
        MyData data = opt.get();
        String result = "<tr><td>" + data.getName()
                + "</td><td>" + data.getMail() + 
                "</td><td>" + data.getAge() + 
                "</td><td>" + data.getMemo() + 
                "</td></tr>";
        return result;
    }

}

Beanがアプリケーションに用意されている状態で、@Autowiredすれば、そのインスタンスが自動的にバインドされる

【Beanを使うためにフィールドに関連付けているコントローラー】

@Autowired
MyDataBean myDataBean;

@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ModelAndView indexById(@PathVariable long id,
        ModelAndView mav) {
    mav.setViewName("pickup");
    mav.addObject("title","Pickup Page");
    String table = "<table>"
            + myDataBean.getTableTagById(id)
            + "</table>";
    mav.addObject("msg","pickup data id = " + id);
    mav.addObject("data",table);
    return mav;
}

ページネーション

SpringFrameworkにはページ分けのためのPageクラスが用意されており、これを利用することで、ページごとにデータを扱う機能であるページネーションを実現できる

パラメータを指定することでページのレコード数や表示するページ番号を指定することができる(2データ/ページでその0ページ目という例)

【アクセス方法】

/page?size=2&page=0

リクエストハンドラであるfindAllメソッドはPagableを引数にしてPageインスタンスを取り出してdatalistという名前でテンプレートに渡している

【ページネーションが実装されているリクエストハンドラをもつコントローラー】

@RequestMapping(value = "/page", method = RequestMethod.GET)
public ModelAndView index(ModelAndView mav, Pageable pageable) {
    mav.setViewName("index");
    mav.addObject("title","Find Page");
    mav.addObject("msg","MyDataのサンプルです。");
    Page<MyData> list = repository.findAll(pageable); //●
    mav.addObject("datalist", list);
    return mav;
}

PageListのサブクラスなので、Listと同じ感覚でテンプレートに渡されたエンティティをテーブルに表示することができる。

【テンプレート】

<table>
<tr><th>ID</th><th>名前</th><th>メール</th><th>年齢</th><th>メモ(tel)</th></tr>
<tr th:each="obj : ${datalist}">
    <td th:text="${obj.id}"></td>
    <td th:text="${obj.name}"></td>
    <td th:text="${obj.mail}"></td>
    <td th:text="${obj.age}"></td>
    <td th:text="${obj.memo}"></td>
</tr>
</table>

Pageableクラスのメソッド

メソッド 戻り
Pageable#getPageNumber() 現在のページ番号
Pageable#getPageSize() ページに表示するレコード数
Pageable#first() 最初のPageable
Pageable#previousOrFirst() 前のまたは最初のPageable
Pageable#next() 次のPageable

Thymelaefの独自タグ作成

 ページネーションをして分けられたエンティティを取れば、ページの前後に移動したり、ページの表示に関する機能を用意する必要があるのでユーティリティオブジェクトを利用してJavaのクラスとして定義してThymeleaf内からその内部のメソッドを呼び出して結果を出力させることができる
 ユーティリティオブジェクトを使うためにはThymeleafの属性を処理するAttributeTagProcessorクラスとこのクラスをまとめるためのDialectクラスを用意する
 
th:text="name"とThymeleafの独自属性を記述したときにtextという属性の処理として値のnameを受け取って処理をするのでその具体的な処理を用意する

AbstractAttributeTagProcessorを継承して作成したクラスにコンストラクタとdoProcessを用意している
このメソッドに渡される引数については以下

引数の型 説明
ITemplatecontext テンプレートのコンテキストを扱う。ここからテンプレートエンジンや設定情報などテンプレートの情報を取り出す。
IProcessableElementTag エレメントタグを扱うクラスでタグに組み込まれている属性などの情報を取り出せる
AttributeName 属性名やプレフィクス(値の前につけられるテキスト)などを扱う
String 属性の値
IElementTagStructureHandler エレメントに属性を組み込むなど構造をハンドリングするクラス

Thymeleafの変数式などが値に記述されている場合テンプレートエンジンに組み込まれているリゾルバという値を評価する機能を使って変数式を処理して、結果となる値を取得してから実際の属性処理を行う必要があるので、以下サンプルはdoProcessで行っている

【AbstractAttributeTagProcessorの継承クラス】

public class MyPageAttributeTagProcessor extends AbstractAttributeTagProcessor {

    private static final String ATTR_NAME = "mypage";
    private static final int PRECEDENCE = 10000;
    public static int size = 2;

    public MyPageAttributeTagProcessor(final String dialectPrefix) {
        super(TemplateMode.HTML, dialectPrefix, null,
                false, ATTR_NAME, true, PRECEDENCE, true);
    }

    protected MyPageAttributeTagProcessor(TemplateMode templateMode, 
            String dialectPrefix, String elementName,
            boolean prefixElementName, 
            String attributeName, 
            boolean prefixAttributeName, 
            int precedence,
            boolean removeAttribute) {
        super(templateMode, dialectPrefix, elementName, 
                prefixElementName, attributeName, prefixAttributeName, 
                precedence,removeAttribute);
    }

    @Override
    protected void doProcess(ITemplateContext context, 
            IProcessableElementTag tag, 
            AttributeName attrName, 
            String attrValue,
            IElementTagStructureHandler handler) {

        final IEngineConfiguration configuration = context.getConfiguration();
        final IStandardExpressionParser parser =
                StandardExpressions.getExpressionParser(configuration);
        final IStandardExpression expression = 
                parser.parseExpression(context, attrValue);
        int value = (int)expression.execute(context);
        value = value < 0 ? 0 : value;
        handler.setAttribute("href", "/page?size=" + size + "&page=" + value);
    }

}

AbstractProcessorDialectにはIProcessorをまとめたSetを取得するgetProcessorsというメソッドが用意されている
IProcessorはThymeleafの属性などの処理を行うクラス
サンプルはSetインスタンスを作成してIProcessorを組み込んでreturnする処理をしている

【Dialectクラスとして属性としてまとめる】

public class MyDialect extends AbstractProcessorDialect {

    private static final String DIALECT_NAME = "My Dialect";

    public MyDialect() {
        super(DIALECT_NAME, "my", StandardDialect.PROCESSOR_PRECEDENCE);
    }
    protected MyDialect(String name, String prefix, 
            int processorPrecedence) {
        super(name, prefix, processorPrecedence);
    }

    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        final Set<IProcessor> processors = new HashSet<IProcessor>();
        processors.add(new MyPageAttributeTagProcessor(dialectPrefix));
        return processors;
    }

}

 AttributeTagProcessorDialectを利用するためのBeanを登録して使う

①templateEngineの作成

 SpringTemplateEngineインスタンスの作成後、setTemplateResolverでテンプレートリゾルバの設定をする
 これによってtemplateResolverで作成されたテンプレートリゾルバが設定される
 用意できたテンプレートエンジンに作成しているMyDialectのインスタンスをaddDialectで組み込み、そのテンプレートエンジンをreturnするとレンダリングの際に使用されるようになる

②templateResolverの作成

 ClassLoaderTemplateResolverのインスタンスを作成してプレフィックスの設定、キャッシュできるようにするかどうかの設定、サフィックスの設定、テンプレートモード等の設定を行う

【構成クラス】

@Configuration
public class MyBootAppConfig {

    ...

    @Bean
    public ClassLoaderTemplateResolver templateResolver() {
        ClassLoaderTemplateResolver templateResolver = 
            new ClassLoaderTemplateResolver();
        templateResolver.setPrefix("templates/");
        templateResolver.setCacheable(false);
        templateResolver.setSuffix(".html");        
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine(){
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.addDialect(new MyDialect());
        return templateEngine;
    }
}

【テンプレート】

<div>
<a my:mypage="${datalist.getNumber() - 1}">&lt;&lt;prev</a><a my:mypage="${datalist.getNumber() + 1}">next&gt;&gt;</a>
</div>

MongoDBの利用

NoSQLのMongoDBは基本的な使い方は、MongoRepositoryがJpaRepositoryと同じように設計されているので、SQLデータベースとほぼ同じ -> リポジトリを書いて呼び出す

8. SpringToolSuiteリファレンス

割愛

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした