#はじめに
Springを業務で使う筆者が、「SpringBoot2 プログラミング入門」を勉強して、まとめたチートシート・読書メモのようなもの。
対象読者は、Javaの基礎が身についていて、SpringBoot未経験者〜基礎を勉強し始めた位の層。
言語はJava、
ビルドツールはMaven、
テンプレートエンジンはThymeleafです。
#SpringBootとは
SpringFrameworkの提供するライブラリとSpringMVC(フレームワーク)を高速なWEBアプリ開発向けに組み合わせてできたフレームワーク(群)
#1. Spring開発のセットアップ
本記事をチートシート的用途とするため、ここでは割愛。
#2. Groovyによる超簡単アプリケーション開発
GroovyはJavaでの本格的な開発前にプロトタイプを短時間で作ることに向いている言語。Groovyについてはここでは割愛。
###@ RestController
外部からアクセスして必要な情報などを取り出すシステムを構築するのに用いられる。
アクセスしたら必要な情報などをテキストやXML形式で送り返すといった用途。
###@ RequestMapping
このアドレスにアクセスされたらこのメソッドを実行するといった意味。
@RequestMapping("/")
のように表記し、引数にパスを指定する。
指定パスにアクセスしたらこのアノテーションがつけられたメソッドが実行されるようになる。
@RestController
public class SampleController {
@RequestMapping("/")
public String index() {
return "Hello SpringBoot World";
}
}
## Thymeleafを利用する
HTMLのタグに、追記して使うことができるテンプレートエンジン。
特徴は、
- タグの中に「th:◯◯」の独自属性を用意
- 「${}」の形をとって変数名を記述して、その場所に値を埋め込む
<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インスタンスと、パラメータとして渡すデータを用意する。
@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ページをレンダリングし、表示するのに用いられる。
@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="/"
のように引数名を省略せずに書く必要がある。サンプルは同じアドレスで、メソッドについてGET
とPOST
で区別している。
フォームから送信された値は、sendメソッドで処理している。@RequestParam
は、フォーム送信された値を指定するためのアノテーションで、これによってフォームのname="text1"
に入力された値がこの引数str
に渡される。
【コンロトーラー側】
@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;
}
【テンプレート側】
<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制約を指定する。
【コントローラー側】
@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>
###リダイレクト
あるアドレスにアクセスしたとき、必要に応じて別のアドレスに移動させたいケースのやり方としてフォワードとリダイレクトがある。
- フォワード:サーバー内部で別のページを読み込み、表示する。アクセスするアドレスはそのままで、表示内容だけが別のページに差し替えられる。
- リダイレクト:クライアント側に送られた後で別のページに移動させる。なのでアクセスしているアドレスも移動先のものに変更される。
【コントローラー側】
@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:/";
}
}
【テンプレート側】
<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クラス定数#dates
のformat
メソッドを使い、第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クラスの定数#numbers
のformatInteger
メソッドを使い、第1引数に整数を、第2引数の桁表示で、第3引数でカンマ区切りにしている
<p th:text="${#numbers.formatInteger(10000000, 3, 'COMMA')}"></p>
Stringクラスの定数#strings
のtoUpperCase
は引数のテキストをすべて大文字に変換するメソッド
<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>
##メッセージ式
プロパティファイルから値を取り出し、テンプレート内で利用する。記述方法は、#{値の指定}
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;
}
【テンプレート側】
<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;
}
【テンプレート側】
<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>
###テンプレートフラグメント
複数のファイルを組み合わせてページを構成するテンプレートフラグメントというやり方。ファイルの中にフラグメントとしてそこだけ切り取って扱えるようにした部品として記述されている部分を別のテンプレートの指定した場所にはめ込む。
【部品となるテンプレート】
<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
とすると読み込みのみのトランザクションから保存などデータの更新を許可するトランザクションに変わる。
@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
コンストラクタによりインスタンスが生成された後に呼び出されるメソッドであることを示す。コントローラーは最初に一度だけインスタンスが作成され、以降はそのインスタンスが保持される。
@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インスタンスを取り出すメソッド。
@Repository
public interface MyDataRepository extends JpaRepository<MyData, Long> {
public MyData findById(Long name):
}
GETアクセス時に呼ばれるeditメソッドでは、クエリで送られたIDを引数とするfindById
によってエンティティを取得して、そのget
を使い、formModel
と名前を指定してaddObject
している。
フォームが/edit
に送信されるとupdateメソッドが呼び出される。送られたフォームのデータを元にエンティティの保存を行う際にも、新たにデータを保存すりのと同じsaveAndFlush
で更新をする。新規保存と更新の違いは、引数のエンティティにIDが指定されているかどうか。
@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
並び順の指定。通常の検索メソッド名の後につけられる。項目名の後にAsc
やDesc
をつけることで昇順か降順かを指定できる。
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をまとめられる |
入力された値が電子メールアドレスかどうかをチェックする | |
@ 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
にはinitialize
とisValid
の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を引数にしてエンティティを取得)
getSingleResult
はQuery
から得られるエンティティを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を返すのでfindByName
はList<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
のパラメータはHttpServletRequest
のgetParameter
を呼び出してパラメータを受け取る操作を自動的に行って、その結果を引数に設定するもの
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を指定して呼び出す
後は、このCriteriaQuery
をcreateQuery
し、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
メソッドを使用する
引数のExpression
はCriteriaBuilder#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
は一人のメンバーがいくつでもメッセージを投稿できるので、
@ManyToOne
でMyData
に関連付けられている
【関連付け元のエンティティ】
@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を利用している場合
<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;
}
Page
はList
のサブクラスなので、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;
}
}
AttributeTagProcessor
とDialect
を利用するための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}"><<prev</a>
|
<a my:mypage="${datalist.getNumber() + 1}">next>></a>
</div>
##MongoDBの利用
NoSQLのMongoDBは基本的な使い方は、MongoRepositoryがJpaRepositoryと同じように設計されているので、SQLデータベースとほぼ同じ -> リポジトリを書いて呼び出す
#8. SpringToolSuiteリファレンス
割愛
#参考