初めに
この記事は主に同期に対してSpringを紹介するために作成されました。
なるべく楽をしながらSpringと仲良くなるのが目的ですので、網羅的な説明は極力避け、目的を達成する手段が幾つかある中から、その内の1つを学ぶ形式で進みます。
また、記事中には正確性に欠ける表現が出現しますが、これも上記と同じ理由です。
前回までのあらすじ
前回はSpringでの画面遷移を学びました。
SpringとServletで大きく異なるのはServletがクラス(class XxxServlet {}
)単位でURLにマッピングするのに対し、Springはメソッド(public String xxx() {}
)単位でURLにマッピングする点でした。
また、JSPとThymeleafはどちらもテンプレートにデータを埋め込む形でページ内容を適切に変更する目的で使用される事が分かりました。
今回はSpringでコントローラーにデータベースを組み合わせる方法を学びます。
データベースアクセス
このチュートリアルでは仮のデータベース(H2 Database)を使うので実際のDBMSに接続はしませんが、MySQL上で実際にデータを保存する体で話を進めます。
この後の流れとしては、MySQL上にデータを保存するためのテーブルを作りCRUDを実装します。
データベースとそのデータベースに全ての権限を持ったユーザーが既に作成されていて、アクセス可能な状態から始めます。
JDBCの場合
比較のために書いているだけなので、JDBCの場合は読み飛ばしてもらっても結構です。
JDBCでのテーブル定義
まず、JDBCではSQLを書いてテーブルを作ります。
/* ユーザーテーブル */
CREATE TABLE user
(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT current_timestamp,
updated_at TIMESTAMP DEFAULT current_timestamp ON UPDATE current_timestamp,
PRIMARY KEY (id)
);
JDBCでのDTO
そして、JavaのクラスとしてDTOを作ります。
import java.util.Date;
public class User {
Long id; // ID
String name; // 名前
Date createdAt; // 作成日時
Date updatedAt; // 更新日時
// コンストラクタ―
public User(Long id, String name, Date createdAt, Date updatedAt) {
this.id = id;
this.name = name;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// IDのゲッター
public Long getId() {
return this.id;
}
// IDのセッター
public void setId(Long id) {
this.id = id;
}
// 名前のゲッター
public String getName() {
return this.name;
}
// 名前のセッター
public void setName(String name) {
this.name = name;
}
// 作成日時のゲッター
public Date getCreatedAt() {
return this.createdAt;
}
// 作成日時のセッター
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
// 更新日時のゲッター
public Date getUpdatedAt() {
return this.updatedAt;
}
// 更新日時のセッター
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
}
JDBCでのDAO
JDBCでのDAOはこんな感じになると思います。ここではユーザーを名前で検索してそのリストを取得するメソッドを例として書きます。
public class UserDao {
/* データベースへのコネクション(接続処理のコードは省略) */
Connection con;
/**
* 名前でのあいまい検索を行う。エラーが発生した場合はnullが返る。
* @param name 検索したい名前
* @return 検索にヒットしたユーザーのリスト
*/
public static List<User> searchWith(String name) {
// SQLでクエリを書く
String query = "SELECT * FROM user WHERE name LIKE ?";
// PreparedStatementを作る
try (PreparedStatement stmt = this.con.prepareStatement(query)) {
// 1つ目の?に名前を中間一致検索できるように%で囲んで設定
stmt.setString(1, "%" + name + "%");
// 値を返すための空の配列リストを作成
List<User> ret = new ArrayList<>();
// ステートメントを実行してリザルトセットを得る
ResultSet rs = stmt.executeQuery();
// リザルトセットのリザルトを1つずつ見ていく
while (rs.next()) {
ret.add( // リストに追加
new User( // 新しいユーザーインスタンスをコンストラクターで作成
rs.getInt("id"), // idカラムからIDを取得
rs.getString("name"), // nameカラムから名前を取得
new Date(rs.getTimestamp("created_at").getTime()), // created_atカラムから作成日時を取得してDate型に変換
new Date(rs.getTimestamp("updated_at").getTime()) // updated_atカラムから更新日時を取得してDate型に変換
)
);
}
return ret;
} catch (SQLException e) {
// 例外が発生したらnullを返す
return null;
}
}
}
このようにDAOクラスを作って、ステートメントを定義してそれを実行、リザルトセットをwhile() {}
で見ながらデータをコンストラクターで1つずつDTOに格納していく、という流れを取るのが基本的かと思います。
Springの場合
さて、同じ内容をSpringの場合についてやっていきましょう。
基本のコンセプトは同じで、SpringでもDAOとDTOを作って、それらを介してデータベースやエンティティにアクセスします。
Springにはデータベースにアクセスするフレームワークがいくつか存在しますが、今回はSpring Data JPAを使います。
Springでのテーブル定義
では、早速をSQLを書いてテーブルの準備を……と行きたい所ですが、実はその必要はありません。
Spring Data JPAにはDTOから自動的にテーブルを生成する設定があるのでそれを利用します。
今回は実際のデータベース接続を行わないので省略しますが、MySQL等のDBMSを使用する際は設定が必要です。
SpringでのDTO
では、DTOを定義していきます。Springでは、DTOは単にエンティティと呼び、エンティティパッケージ(entities
)に含めます。
package com.example.trainwithspring.entities;
import java.time.OffsetDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity // このクラスがエンティティである事を明示
@Table // このエンティティをDBに保存する事を明示
@Getter // クラスのフィールド全てにゲッターを作る
@Setter // クラスのフィールド全てにセッターを作る
@NoArgsConstructor // 引数無しのコンストラクタを作る
@AllArgsConstructor // 全てのフィールドを引数に取るコンストラクタを作る
public class User {
@Id // このフィールドが主キーである事を明示
@GeneratedValue // このフィールドは自動生成される事を明示
private Long id;
@Column(nullable = false, unique = true) // テーブルでこのカラムは非NULL且つ一意である事を明示
private String name;
@Column(updatable = false, nullable = false) // テーブルでこのカラムはNULLに出来ず更新も不可能である事を明示
@CreationTimestamp // データベースにエンティティを記録した初回に自動で現在日時を設定する
private OffsetDateTime createdAt;
@Column(nullable = false)
@UpdateTimestamp // データベースにエンティティを記録/上書きした時に自動で現在日時を設定する
private OffsetDateTime updatedAt;
}
一見長く見えますが大半はインポートとコメントです。
Spring Data JPAでは、アノテーション(@
から始まるラベル)で、クラスやフィールドに注釈を付ける事で、様々な機能を自動で実装できます。
上記のコードは実際に「JDBCでのテーブル定義」と「JDBCでのDTO」と全く同じ機能を有しています。
@Getter
/@Setter
でゲッター/セッターが自動実装されるのは最初は慣れないかもしれませんが、普通にゲッター/セッターを1つずつコードとして書く動作がアノテーションを書く動作に変わるだけです。
ちなみに、ゲッター/セッターのような毎回決まった内容だけど必ず書く必要のある決まり文句的なコードの事を『ボイラープレート』または『ボイラープレートコード』と呼びます。
SpringでのDAO
エンティティ(DTO)を実装できたので、今度はSpring Data JPAでDAOを実装しましょう!
Spring Data JPAでは、DAOをリポジトリ(Repository)と呼び、リポジトリパッケージ(repositories
)に作成します。
では、早速、先ほどの名前でユーザーを検索するDAOを実装します!
package com.example.trainwithspring.repositories;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import com.example.trainwithspring.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByNameContaining(String name);
}
完成です。
「え?」って思った人は正常なので安心してください。
Javaを学んでいる人には、これがクラスではなくインターフェースで、実装すべきメソッドの宣言のみを行っているように見えると思います。
確かにメソッドの名前を宣言しただけで何も実装はしていないので、これで完成と言われても信じられないかもしれません。
ですが、本当にSpring Data JPAにおけるDAOはこれで完成です。
秘密はここにあります。
... extends CrudRepository<User, Long> { ...
Spring Data JPAが提供するCrudRepository<T, ID>
を継承したインターフェースを作成しておくと、Springが起動時に自動的にメソッドを実装します。
今回、CrudRepository<User, Long>
を継承しているのは、このリポジトリ(DAO)がさっき作ったUser
エンティティのためで、User
エンティティの主キーid
の型がLong
である事が理由です。
例えば、User
エンティティの主キーの型がUUID
の場合は、CrudRepository<User, UUID>
になります。
そして、CrudRepository<T, ID>
を継承したインターフェースでは、メソッドの名前に応じて、処理が自動実装されます。
今回はfindBy
(検索する) Name
(User
のname
に) Containing
(が含まれる)としたので、ユーザーの名前の中に指定した文字列が含まれるユーザーを取得する処理が自動実装されました。
リポジトリのクエリメソッド
上記のfindByNameContaining
の構文は、以下のページに解説があります。
Spring Data JPAではこのページを参考に、欲しい処理をSQLではなくメソッド名で指定する事で自動実装してもらいます。
もちろんSQLでクエリを書いて自分の思い通りのデータを持って来る事も出来ますが、利用するデータベース(MySQL, PostgreSQL, MariaDB, ...)に依存しやすいアプリケーションになってしまうので注意が必要です。
戻り値について
今回は複数のユーザーを取得したかったので、List<User>
をメソッドの戻り値にしましたが、例えば一件だけ取得するようなメソッドの場合はOptional<User>
を戻り値にします。
Optional<T>
については、null
になりそうな戻り値に対してnull
である可能性がある旨を明示するためのクラスなので、余計なバグの抑止に繋がります。
Springでのリポジトリ(DAO)のコントローラーでの使い方
ユーザーからアクセスがあった場合にDBのデータを参照してその結果をページに表示する、という処理はよくあると思います。
Servletでは何らかの手段でDAOをServlet内から呼び出して、使いたいメソッドを使って、という手順だと思いますが、
Springでもリポジトリ(DAO)をコントローラー(Servletにおける〇〇Servlet
)から呼び出して、使いたいメソッドを使うという手順を取ります。
以下は、さっき作った名前検索をコントローラー内で呼び出す場合のコードです。
package com.example.trainwithspring.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.trainwithspring.entities.User;
import com.example.trainwithspring.repositories.UserRepository;
@Controller
public class UserController {
@Autowired // このフィールドはSpringで勝手に用意してくれという意味のおまじない
UserRepository userRepository;
/**
* http://localhost:8080/userにGETでアクセスがあった時に呼ばれる。
* 名前であいまい検索をした結果のページを返す。
* @param name 検索する名前
* @param model Thymeleafに渡すデータを格納するための入れ物
* @return Thymeleafで参照するテンプレート
*/
@GetMapping("/user")
public String searchWith(
@RequestParam(name = "name", required = true) String name,
Model model) {
// UserRepositoryで先ほど定義した名前検索を使う
List<User> users = userRepository.findByNameContaining(name);
model.addAttribute("users", users);
return "search-result";
}
}
さて、DAOの使い方はServletとほぼ同一だと思いますが、Springでは一点だけ気を付ける所があります。
@Autowired
@Autowired
とは、
……と説明したいところですが、ここではリポジトリをコントローラーで使う時はコントローラーのフィールドとしてリポジトリを定義しその上に@Autowired
と書くとだけ覚えておけば十分だと思います。
@Autowired
が付いたフィールドはSpringが自動的に初期化しますので、コンストラクタで明示的にthis.service = service
みたいにする必要はありません。
後でフィールドに@Autowired
と付けると何が起こるのか知りたくなった時は『依存性の注入(Dependency Injection)』について調べてみてください。
リポジトリのその他の自動実装機能
他にも、データを永続化する.save()
や削除する.delete()
等が、CrudRepository<T, ID>
を継承した時点、
つまり特に何も追記しなくても自動的に実装されます。
以下は上記のようなデフォルトの自動実装メソッドを使う例です。
package com.example.trainwithspring.controllers;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.trainwithspring.entities.User;
import com.example.trainwithspring.repositories.UserRepository;
@Controller
public class UserController {
@Autowired // このフィールドはSpringで勝手に用意してくれという意味のおまじない
UserRepository userRepository;
/**
* 新規ユーザーの登録をする。
* @param name 登録するユーザー名
* @param model Thymeleafに渡すデータを格納するための入れ物
* @return Thymeleafで参照するテンプレート
*/
@PostMapping("/register-user")
public String register(
@RequestParam(name = "name", required = true) String name,
Model model) {
User user = new User();
user.setName(name);
/* 【自動実装】ユーザーをデータベースに保存 */
userRepository.save(user);
return "register-result";
}
/**
* ユーザーを削除する。
* @param id 削除するユーザーのID
* @param model Thymeleafに渡すデータを格納するための入れ物
* @return Thymeleafで参照するテンプレート
*/
@DeleteMapping("/delete-user")
public String delete(
@RequestParam(name = "id", required = true) Long id,
Model model) {
/* 【自動実装】ユーザーを主キー(今回はLong型のid)で1人取得 */
// Optional<>は値がnullかもしれないという事を明示するクラス
Optional<User> optUser = userRepository.findById(id);
// 値がnullである(指定したIDのユーザーが見つからない)
// Optional<>のnullチェックはもっと色々な書き方があるがひとまずこれが最も基本的なJavaに近い
if(optUser.isEmpty()) {
// 今回は実装しないがユーザーが見つからない旨を示すページ(user-not-found.html)に飛ばしてみたり
return "user-not-found";
}
User user = optUser.get();
/* 【自動実装】ユーザーを削除 */
userRepository.delete(user);
return "delete-result";
}
}
サービス
Spring Data JPAのリポジトリで出来る基本的なCRUDや検索の範囲を超えた、少し複雑な処理をしたい場合があります。
例えば、エンティティを保存する前に何かデータに一定の処理を施したいとか。
そういう場合に役立つのがサービスです。サービスは基本的にリポジトリを内包して、リポジトリでは難しい少し複雑な処理をする際に作成します。
Springでサービスを作る際は、サービスが提供するメソッドを定義するインターフェースとその実装クラスの2種類を用意します。
サービスのインターフェース
先ほどのリポジトリもインターフェースとして定義しましたが、リポジトリをインターフェースとして定義したのはSpringに自動的に実装を用意してもらうためでした。
一方、サービスのインターフェースは自動実装のためではなく、本来のJavaのインターフェース(interface
)として利用します。
個人開発でインターフェースは恩恵が薄いように感じるかもしれませんが、Springでは先ほどの@Autowired
を用いて、ソースコードに変更を加えずにテスト実装と本実装を切り替えたりといった事を行います。
では、サービスのインターフェースを定義します。
package com.example.trainwithspring.services;
import java.util.NoSuchElementException;
import com.example.trainwithspring.entities.User;
public interface UserService {
/**
* ユーザーが見つかれば取得、見つからない場合は例外を投げる。
* @param id
* @return
*/
public User getById(Long id) throws NoSuchElementException;
/**
* ユーザーを記録する時に名前に「さん」を付けて保存する。
* 「山田」⇒「山田さん」としてデータベースに保存されるようになる。
* @param user
* @return
*/
public User saveWithRespect(User user);
}
このインターフェースには2つのメソッドが定義されています。
1つは、ユーザーが見つかった場合は取得しそうでない場合は例外を投げるメソッドです。
もう1つは、ユーザーを保存する時にそのユーザー名に「さん」を付けて敬意を込めるメソッドです。
サービスの実装クラス
では、先ほどのインターフェースに沿って実装を用意します。
慣例的に〇〇Service
の実装クラスは〇〇ServiceImpl
と命名します。
Impl
は文字通り実装(Implementation)を表す接尾辞です。
そして、サービスの実装クラスには@Service
アノテーションを付けて、サービスである事を明示します。
こうする事で、@Autowired
が付いた〇〇Service
フィールドが〇〇ServiceImpl
のインスタンスで自動的に初期化されるようになります。
package com.example.trainwithspring.services.impl;
import java.util.NoSuchElementException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.trainwithspring.entities.User;
import com.example.trainwithspring.repositories.UserRepository;
import com.example.trainwithspring.services.UserService;
@Service // このクラスがサービスの実装クラスである事を明示
public class UserServiceImpl implements UserService {
@Autowired // Springに勝手にやってもらうためのおまじない
UserRepository userRepository;
@Override
@Transactional(readOnly = true) // 共有ロック(この処理の実行中に他の処理によるデータの変更をブロック)
public User getById(Long id) throws NoSuchElementException {
return userRepository.findById(id).orElseThrow();
}
@Override
@Transactional(readOnly = false) // 占有ロック(この処理の実行中に他の処理によるデータの閲覧/変更をブロック)
public User saveWithRespect(User user) {
user.setName(user.getName() + "さん");
return userRepository.save(user);
}
}
インターフェースに含まれる2つのメソッドを実装しました。
サービスの使い方
こちらも、リポジトリと使い方は全く変わりません!
@Autowired
を付けたサービスを格納するフィールドを定義するだけです。以降の使い方はリポジトリや普通のJavaクラスのインスタンスと全く同じです。
今回は@Service
の付いた実装クラスが一つだけなので、先ほど用意した実装クラスのインスタンスが自動的にフィールドに入ります。
package com.example.trainwithspring.controllers;
/* 省略 */
@Controller
public class UserController {
@Autowired // このフィールドはSpringで勝手に用意してくれという意味のおまじない
UserRepository userRepository;
@Autowired // サービスを使う時ももちろん@Autowired
UserService userService;
/* 省略 */
}
まとめ
今回はSpring Data JPAについて学びました。
重要なのはリポジトリはインターフェースで定義すると自動実装してくれる事、使う時は@Autowired
を付けてフィールドを定義する事です。
次回はSpring Data JPAについてもう少し学びます。