ダウンロードしたファイルを展開する。
[ファイル]-[新規]-[Springスタータープロジェクト]
「Spring Bootバージョン」を「2.7.11」
※以下はデータベース使用時
完了を押し、インポートが終わるまでしばらく待つ。
dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
プロジェクトを右クリックし[実行]-[Spring Bootアプリケーション]
ブラウザ http://localhost:8080 にアクセス("/"マッピング時)
2回目以降は停止して実行、あるいはRelaunchボタン
コードの断片を素早く挿入できる機能。
SpringBoot用スニペットダブルクリックするとカーソル位置に挿入されます。
パラメータがあるスニペットは値を設定して挿入します。
[src/main/java]内のパッケージ内に通常のクラスを新規に作成。
@Controller を付け、メソッドにURLを割り当てる。
@GetMapping でURLを設定する(POSTのときには @PostMapping)。
@Controller public class HanbaiController { @GetMapping("/slist") public String slist() { return "slist"; } }
この例ではURLが /slist 、メソッドはGET でアクセスすると slistメソッドが呼ばれる。
戻り値で表示したいhtmlファイルの名前を指定する( slist.html なら slist だけでよい )。
htmlファイルはresourcesのtemplate内に置く。
htmlファイルにオブジェクトを送るには、メソッドの引数にModelクラスのインスタンスを指定する。これのaddAttributeメソッドで渡したいデータを名前と共に指定する。
@Controller public class SampleController { @GetMapping("/index") public String index(Model model) { // num という名前で10を渡す。 model.addAttribute("num", 10); return "index"; } }
このときに指定した名前でhtml内で以下のように指定する。
<p th:text="${num}"></p>これは以下のようになる。
<p>10</p>
コントローラでデータ設定時、名前は省略も可能。省略時にはクラスの先頭文字を小文字にしたものになる。また、リストの場合、リストの要素型+Listになる(先頭は小文字)。
@Controller public class SampleController { @GetMapping("/index") public String index(Model model) { Shouhin s = new Shouhin(); model.addAttribute(s); // 名前はshouhin List<Shouhin> list = new ArrayList<>(); model.addAttribute(list); // 名前はshouhinList return "index"; } }
URLにパラメータを付けてアクセスする(例:/uriage/3 )し、その値をコントローラで受け取る。
@GetMapping("/uriage/{sid}") public String uriage(@PathVariable int sid, Model model) { List<Uriage> list = repository.findBySid(sid); model.addAttribute(list); return "uriage"; }
@GetMapping のURLに{名前}を書く。URLのこの部分に入れた値をメソッドの引数「@PathVariable 変数型 変数名」で受け取る。
フォームからの受け取りはメソッドの引数で行う。
// HTML <form method="post" action="slist"> 商品名:<input type="text" name="sname" ><br> 単価:<input type="text" name="tanka" ><br> <input type="submit" value="追加"> </form>
上のフォームからsnameとtankaが渡される。
これを下のメソッドの引数、snameとtankaで受け取る。
// Java @PostMapping("/insert") public String insert(String sname ,Integer tanka) { Shouhin s = new Shouhin(); s.setSname(sname); s.setTanka(tanka); repository.save(s); // データベース保存 return "redirect:/slist"; }
なお、同nameが複数ある場合、配列で受け取ることが出来る。
String[] sname , // snameが複数ある
フォームから入力された内容を自動的にオブジェクトに設定できる。
※フォームのnameとオブジェクトのフィールド名が一致していること。nameが存在しない場合、フィールドに値が設定されない(nullなど)。
@PostMapping("/insert") public String insert( Shouhin s) { repository.save(s); // データベース保存 return "redirect:/slist"; }
注:日付をこれで受け取るためには属性に@DateTimeFormatを付けて形式を指定する。
// formがtype="date"時の設定 @DateTimeFormat(pattern="yyyy-MM-dd") private Date hi;
別ページにリダイレクトする場合、redirect:を付ける。
return "redirect:/slist";
フォーム内に複数ボタンを配置し、どのボタンを押したかにより処理を振り分けます。まず、フォームのボタンにname属性を付けます。
<form method="post" action="insert"> 商品名:<input type="text" name="sname"><br> 単価 :<input type="text" name="tanka"><br> <input type="submit" value="追加" name="ok"> <input type="submit" value="キャンセル" name="cancel"> </form>
コントローラ内のメソッドではルーティングのアノテーションで、paramasを設定し、ボタンに付けたnameで呼ばれるメソッドを分けます。
@PostMapping(value="/insert", params="ok") public String insert(Shouhin s) { repository.save(u); return "redirect:/slist"; } @PostMapping(value="/insert", params="cancel") public String insert_cancel() { return "redirect:/"; }
動的なhtmlファイルは、resource/templates に、静的なhtmlはresource/staticに作る。
htmlファイルにオブジェクトを送るには、Modelに addAttributeでオブジェクトを渡す。その際に名前を付けて渡す。
String mes = "こんにちは"; // 名前 mes で、オブジェクトmesを渡した model.addAttribute("mes",mes); return "slist";
htmlファイルではthymeleafの書式を使って渡されたオブジェクトを表示する。
具体的には ${オブジェクト名} で取得できる。これをth:で始まる属性にセットする。
Eclipseではプロジェクトを右クリックし[Thymeleaf]-[Thymeleaf ネーチャーの追加]でHTML編集画面でth:の後にCtrl+Spaceを押すと選択肢がでるようになる。
タグに囲まれた部分に出したい場合、タグのth:text属性にセットする。
<p th:text="${mes}"></p>
上の例の場合、以下のようにHTMLが生成される(mesに「こんにちは」が入っている場合)。
<p>こんにちは</p>
<p>氏名:[[${shimei}]]</p>
オブジェクトのメソッドに getXxx() というものがある場合、"オブジェクト名.xxx" だけで表示できる。
例:shouhinにgetSname()がある場合
<p th:text="${shouhin.sname}"></p>
th:属性名="${名前}"
<a th:href="|del/${shouhin.sid}|">削除</a>
<input type="hidden" name="sid" th:value="${shouhin.sid}">
th:selectedに指定した条件がTrueならseleceted
<option value="0" th:selected="${shouhin.cid==0}">
th:checkedに指定した条件がTrueならchecked
<input type="radio" value="0" th:selected="${shouhin.cid==0}">
inputタグの場合、th:fieldを指定すると自動的にname属性とvaue属性(とid属性)を設定できる。
<input type="hidden" th:field="${shouhin.sid}"> これは以下と同じ。 <input type="hidden" name="sid" th:value="${shouhin.sid}">
selectタグに指定した場合、nameを自動的に設定し、optionタグの対応するものをselectedしてくれる。
shouhin.sidが2の場合、value="2" のあとに自動的に selected が入る。 <select th:field="${shouhin.sid}"> <option value="1">りんご</option> <option value="2">みかん</option> <option value="3">いちご</option> </select>
List<Shouhin> list = repository.findAll(); model.addAttribute("list",list); return "slist";
上のコードでlistをhtmlファイルに送った場合、以下のようにして表示する。
<table> <tr th:each="shouhin : ${list}"> <td th:text="${shouhin.sid}"></td> <td th:text="${shouhin.sname}"></td> <td th:text="${shouhin.tanka}">円</td> </tr> </table>
th:eachを書いたタグとそれに囲まれた部分がリストの要素数だけ繰り返される。
リストの一個の要素はshouhinに入るので ${shouhin.sid}のようにして表示する。
リストの数により処理を分けたい場合、th:ifを使って以下のように行う。
<div th:if="${#lists.isEmpty(list)}"> リストに一個も無い時 </div> <div th:if="!${#lists.isEmpty(list)}"> リストに一個以上ある時 </div> <div th:if="${#lists.size(list)}>3" > リストに3個より多い要素がある時 </div>
繰り返しのインデックスなどを知りたい場合、以下のようにして表示する。
<tr th:each="shouhin , stat : ${list}"> <td th:text="${stat.count}"></td> <!-- 1始まりのカウント --> <td th:text="${stat.index}"></td> <!-- 0始まりのカウント --> <td th:text="${stat.size}"></td> <!-- 全要素数 --> <td th:text="${shouhin.sname}"></td> </tr>
リストの0番目などと指定して表示したい場合、配列のように[番号]で表示する。
<td th:text="${list[0].sname}"></td>
||で囲むと、その中の文字列中に ${オブジェクト} の値が埋め込まれる。
例:|del/${shouhin.sid}|
shouhin.sidが1なら
del/1
になる。
// リンクの例 <a th:href="|del/${shouhin.sid}|">削除</a>
<td th:text="${#dates.format(uriage.hi,'yyyy/MM/dd')}"></td>
条件によって出す文字列を変えたいときには三項演算子を使う
条件 ? Trueのときの値 : Falseのときの値
<p th:text="${shouhin.tanka>100 ? '高い' : '安い'}"></p>
th:ifに指定した条件がTrueならそのタグが表示、そうでないなら非表示
<p th:if="${shouhin.tanka>100}">高い</p> <p th:if="${shouhin.tanka<100}">安い</p>
別ファイルの一部を読み込むことが出来る
例:head.htmlの一部<header th:fragment="header"> <h1>ヘッダ</h1> <p>ヘッダ部分</p> </header>これを読み込む
<header th:include="head::header"></header>
プロジェクト作成時、Spring Data JPAとMySQLをチェックしていなかったとき、 build.gradleのdependencies内に以下を追加
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.mysql:mysql-connector-j'
application.propertiesファイルに設定を書く。
spring.datasource.url=jdbc:mysql://localhost:3306/データベース名 spring.datasource.username=ユーザ名 spring.datasource.password=パスワード spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.jpa.database=MYSQL spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mysql://localhost:3306/hanbai spring.datasource.username=root spring.datasource.password= spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.jpa.database=MYSQL spring.jpa.hibernate.ddl-auto=none
テーブルの一行を表すエンティティクラスを作る。@Entityを指定する。
主キーには@Idを指定する。
※インポートは javax.persistence.*
@Entity public class Shouhin { @Id private Integer sid; private String sname; private Integer tanka; // あとはgetter setter }
なお、テーブル名とクラス名が異なる場合、@Tableでテーブル名を指定する
列名とフィールド名が異なる場合、@Columnで列名を指定する。
auto_incrementの列には@GeneratedValue(strategy=GenerationType.IDENTITY)のように連番方法を指定する。
@Entity @Table(name="Shouhin") public class Shouhin { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name="sid") private int sid; @Column(name="sname") private String sname; @Column(name="tanka") private int tanka; // あとはgetter setter }
※java.util.Date型は以下のように日付の書式を指定しておくことで input type="datetime-local" の部品とやりとりを行える。
@DateTimeFormat(pattern="yyyy-MM-dd'T'HH:mm") private Date nichiji;
日付のみの場合、以下の指定で input type="date"の部品とやりとりを行える。
@DateTimeFormat(pattern="yyyy-MM-dd") private Date hi;
Lombokを使うとGetter、Setterやコンストラクタは自動で生成される。
準備:build.gradleのdependenciesに以下を追加(新規プロジェクト作成時には「開発ツール」の「Lombok」にチェック)
compileOnly 'org.projectlombok:lombok'
エンティティに@Dataを付けると、getter、setterなどが自動生成される
@Data // getter, setter, toString, equals, hashCode @NoArgsConstructor // 引数無しコンストラクタ @AllArgsConstructor // 全てのフィールドを引数に持つコンストラクタ @Entity public class Shouhin { @Id private int sid; private String sname; private int tanka; }
データベースを操作できるrepositoryインタフェースを作る。
これはJpaRepositoryを継承する(エンティティクラス名と主キーのクラス名を指定)。これだけである程度の操作は可能。
@Repository public interface ShouhinRepository extends JpaRepository <Shouhin, Integer> { }
Controller内でrepositoryを実体化し、操作する。実体化は @Autowired で自動的に出来る。
@Controller public class HanbaiController { @Autowired private ShouhinRepository repository; @GetMapping("/slist") public String slist(Model model) { List<Shouhin> list = repository.findAll(); model.addAttribute("list",list); return "slist"; } }
// 全行取得 List<Shouhin> list = repository.findAll(); // 主キーから一件取得 Shouhin s = repository.findById(sid).get(); // 追加・更新 repository.save(s); // 主キーによる削除 repository.deleteById(sid); // 全件数取得 long count = repository.count(); // 主キーによる存在確認 repository.existsById(sid);
※findByIdはOptional<Shouhin>を返す。
get()でShouhinを取得できる。存在しない場合、例外 NoSuchElementException が起きる。
isPresent()でnullだったかどうかが分かる(falseならnull)
// 例外でエラー処理する例 try { Shouhin s = repository.findById(sid).get(); model.addAttribute("shouhin",s); }catch(NoSuchElementException e) { return "err"; }
// isPresentで判定する例 Optional<Shouhin> s = repository.findById(sid); if( s.isPresent()) { model.addAttribute("shouhin",s.get()); }else { return "err"; }
// orElseで存在しないときにnullを入れて判定する例 Shouhin s = repository.findById(sid).orElse(null); if( s == null) { return "err"; } model.addAttribute("shouhin",s.get());
他にエンティティ数を返す long count() や、IDのエンティティが存在するかを調べるboolean existsById(id) などがある。
※saveで追加後、オブジェクトには自動連番など追加後の値がセットされる。
※find系で取得した後、オブジェクトを変更したら、flushで保存できる。
// 主キーから一件取得 Shouhin s = repository.findById(sid).get(); s.setTanka(30); // 単価変更 repository.flush(); // 保存
// tankaの昇順 List<Shouhin> list = repository.findAll(Sort.by("tanka")); // tankaの降順 List<Shouhin> list = repository.findAll(Sort.by(Sort.Order.desc("tanka"))); // tankaの昇順、同じならsidの昇順 Listlist = repository.findAll(Sort.by("tanka","sid")); // tankaの昇順、同じならsidの降順 List list = repository.findAll(Sort.by(Sort.Order.asc("tanka"),Sort.Order.desc("sid")));
ルールに合わせた名前を宣言するだけで自動的に実装される。
public interface UriageRepository extends JpaRepository <Uriage, Integer> { // 指定されたsid List<Uriage> findBySid(int sid); // 指定された日付 List<Uriage> findByHi(Date hi); // 指定されたsidと日付 List<Uriage> findBySidAndHi(int sid,Date hi); // 指定されたsidまたは日付 List<Uriage> findBySidOrHi(int sid,Date hi); // 指定された個数より小さい List<Uriage> findByKosuLessThan(int kosu); // 指定された個数より大きい List<Uriage> findByKosuGreaterThan(int kosu); // 日付がNull List<Uriage> findByHiIsNull(); // 日付がNullではない List<Uriage> findByHiIsNotNull(); // 指定された日付をLikeで List<Uriage> findByHiLike(Date hi); // 指定されたsidを日付の降順 List<Uriage> findBySidOrderByHiDesc(int sid); }
public interface ShouhinRepository extends JpaRepository <Shouhin, Integer>{ // snameを含むもの List<Shouhin> findBySnameContains(String sname); // snameを含むものを指定ソート順で List<Shouhin> findBySnameContains(String sname,Sort sort); // snameで存在確認 boolean existsBySname(String sname); }※Sortは以下のように使う。
List<Shouhin> list = repository.findBySnameContains("ん",Sort.by(Direction.DESC,"tanka"));
@QueryでSQLを指定し実行することも出来る。
// 個数2個以上 @Query(value="SELECT * FROM uriage WHERE kosu>=2", nativeQuery = true) List<Uriage> findKosu();
引数を使う場合、SQL内に?番号で引数の順番を指定する。
@Query(value="SELECT * FROM uriage WHERE kosu >= ?1", nativeQuery = true) List<Uriage> findKosu(int kosu);
もしくは、@Param(名前)を指定し、SQL内に :名前 で指定する。
@Query(value="SELECT * FROM uriage WHERE kosu > =:kosu", nativeQuery = true) List<Uriage> findKosu(@Param("kosu") int k);
集計関数を使う場合など単一値を返す場合、Integerなどを戻り値の型に指定する。
@Query(value="SELECT sum(kosu) as cnt FROM uriage", nativeQuery = true) Integer sumKosuAll();
独自の列がある場合、それを格納するためにリポジトリ内にinterfaceを作り戻り値の型にする。
public interface UriageRepository extends JpaRepository <Uriage, Integer>{ @Query(value="SELECT sid,sum(kosu) as cnt FROM uriage GROUP BY sid", nativeQuery = true) List<Result> sumKosu(); public interface Result { public int getSid(); public int getCnt(); } }
// 呼び出し側の例 List<UriageRepository.Result> results = repository.sumKosu();
更新系の場合、@Modifyingを付ける。
@Modifying @Query(value="DELETE uriage WHERE sid=:sid", nativeQuery = true) Integer deleteBySid(@Param("sid") int sid);
findAllや、count、existsメソッドの引数に Example.of(エンティティ) を指定することで、エンティティの内容と完全一致するもののみを検索対象に出来る。エンティティ内のnullの項目は無視される。
Shouhin s = new Shouhin(); s.setSname("りんご"); s.setTanka(100); List<Shouhin> list = repository.findAll(Example.of(s)); // 「りんご」かつ100円のみ検索
Example.ofの第二引数に、ExampleMatcherを指定することで、検索方法のカスタマイズが出来る。
Shouhin s = new Shouhin(); s.setSname("りんご"); s.setTanka(100); // 「りんご」または100円のみ検索 ExampleMatcher em = ExampleMatcher.matchingAny(); List<Shouhin> list = repository.findAll(Example.of(s, e));
Shouhin s = new Shouhin(); s.setSname("ん"); ExampleMatcher em = ExampleMatcher.matching() .withStringMatcher(StringMatcher.CONTAINING); List<Shouhin> list = repository.findAll(Example.of(s, e)); // 「ん」を含むものごのみ検索
CONTAINING以外に STARTING(前方一致)、ENDING(後方一致)がある。
.withIgnorePaths(列名,,,) | 列名を無視 |
.withIncludeNullValues(列名,,,) | nullの列も比較 |
.withMatcher(列名, 変数名 -> 変数名.メソッド名()) | その列のみを指定メソッドで比較 |
エンティティ内に結合するクラスの変数を宣言し、以下のアノテーションを付ける。
1対1 | @OneToOne | 結合先のオブジェクト1つ |
1対他 | @OneToMany | 結合先のオブジェクトのリスト |
多対1 | @ManyToOne | 結合先のオブジェクト1つ |
多対多 | @ManyToMany | 結合先のオブジェクトのリスト |
さらに@JoinColumnも付ける。
name | 参照元テーブルの外部キー列名 |
referencedColumnName | 主キーの列名 |
insertable | 挿入時にこのオブジェクトのキーを使う。別にキー列のフィールドがある場合、false |
updatable | 更新時にこのオブジェクトのキーを使う。別にキー列のフィールドがある場合、false |
これを付けることにより、リポジトリからそのエンティティを読み込んだとき、自動的に結合先のエンティティも読み込む。
なお、リポジトリを通していないとき(自分でnewしたときやフォームから得たとき)には、自動では結合してくれないので、手動で結合先のエンティティを得る。
Uriageクラスに以下を宣言すると、リポジトリからの読み込み時に自動的にsidで結合したShouhinを取得。
@ManyToOne @JoinColumn(name = "sid",referencedColumnName = "sid", insertable = false,updatable = false) private Shouhin shouhin;
Uriageクラスに以下を宣言すると、リポジトリからの読み込み時に自動的にsidで結合したShouhinを取得。
@OneToMany @JoinColumn(name = "sid",referencedColumnName = "sid", insertable = false,updatable = false) private List<Uriage> uriageList; public List<Uriage> getUriageList() { return uriageList; }
@OneToMany @Where(clause = "kosu > 1") @JoinColumn(name = "sid",referencedColumnName = "sid", insertable = false,updatable = false) private List<Uriage> uriageList; public List<Uriage> getUriageList() { return uriageList; }
@OrderBy("hi desc") のようにして整列順序も指定できる。
※importは javax.persistence.OrderBy
例外をキャッチするハンドラとなるメソッドを定義できる。例えば、フォームからの送信データをオブジェクトにバインドできなかったときはBindExceptionが発生するので以下のように書ける。
@ExceptionHandler(BindException.class) public String handleBindException(BindException e, Model model) { model.addAttribute("msg", "正しく入力を行ってください。"); return "err"; }
err.htmlには以下のように書く
<h1>エラー</h1> <p th:text="${msg}"></p> <button type="button" onclick="history.back()">戻る</button>
どのような例外かは以下で分かる
for(ObjectError oe : e.getAllErrors()) { System.out.println(oe.getCode()); // エラーの種類 System.out.println(oe.getObjectName()); // エラーを起こしたオブジェクト }
build.gradleのdependenciesに以下を追加(新規プロジェクト作成時には「I/O」の「検証」にチェック)
implementation 'org.springframework.boot:spring-boot-starter-validation'
エンティティにアノテーション設定
@Entity public class Shouhin { @Id private int sid; @NotEmpty(message="商品名を入力してください") private String sname; @Min(0) private int tanka;
コントローラにはエンティティ前に@Validatedを指定し、その直後にBindingResultを受け取るようにする。
エラーがあるときには元の画面にリダイレクト。
@PostMapping("/insert") public String insert(@Validated Shouhin s, BindingResult result, RedirectAttributes attr) { if( result.hasErrors() ) { attr.addFlashAttribute(BindingResult.MODEL_KEY_PREFIX +"shouhin", result); attr.addFlashAttribute(s); return "redirect:/shouhin" } repository.save(s); return "redirect:/shouhin"; }
テンプレートでのエラー表示。
<div th:if="${shouhin}" th:object="${shouhin}" > <form action="/insert" method="post"> 商品名:<input type="text" name="sname" value=""> <span th:if="${#fields.hasErrors('sname')}" th:errors="*{sname}"></span> <br> 単価:<input type="text" name="tanka" value=""> <span th:if="${#fields.hasErrors('tanka')}" th:errors="*{tanka}"></span> <br> <input type="submit" value="追加"> </form> </div>
まとめてエラー表示する場合(formタグの中に書く)
<div th:if="${shouhin}" th:object="${shouhin}" > <ul th:if="${#fields.hasErrors('*')}"> <li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li> </ul> </div>
エラーメッセージを変更する場合、resource配下に messages.properties を作成し、以下のように書く(反映されない場合、Eclipse再起動)。
typeMismatch.tanka=単価は整数で入力してください。 typeMismatch.int=整数で入力してください。
コントローラでエラーを追加したい場合、以下のように書く(snameでエラー時)。
result.addError(new FieldError("shouhin","sname","商品名が不正です"));
コントローラで個別にエラーを処理したい場合、以下のように書く(tankaで型変換エラー時)。
FieldError er = result.getFieldError("tanka"); if( er != null && er.getCode().equals("typeMismatch")) { // tankaのエラー時の処理 }
Autowiredでセッションも自動的にインスタンス化できる。
@Controller public class HanbaiController { @Autowired private HttpSession session; @GetMapping("/del/{sid}") public String del(@PathVariable int sid) { Shouhin s = repository.findById(sid).get(); // セッションに入れる。 session.setAttribute("del",s); return "del"; } @PostMapping("/del") public String shouhinDel() { // セッションから取り出す Shouhin s = (Shouhin)session.getAttribute("del"); // 削除処理 repository.delete(s); // セッションから削除 session.removeAttribute("del"); return "redirect:/slist"; } }
暗黙オブジェクト session を使用する
// 表示 <div th:text="${session.shouhin.sname}"></div> // セッションによる判定 キーがあるとき <div th:if="${session.shouhin}"></div> // セッションによる判定 キーが無いとき <div th:if="${session.shouhin==null}"></div>
リダイレクト先で一度のみ表示するメッセージ。セッションを利用して行う。
コントローラの最後の引数にRedirectAttributesを追加。そのaddFlashAttributeメソッドでメッセージ追加
@PostMapping("/insert") public String insert(Shouhin s, RedirectAttributes attr) { repository.save(s); // フラッシュメッセージの追加 attr.addFlashAttribute("message", "商品を追加しました"); return "redirect:/shouhin"; }
表示はmodelにaddAttributeしたときと同じ
<p th:if="${message}" th:text="${message}"></p>