はじめに
Java言語の学習目的で今回はCRUD機能を持ったアプリを制作します
CRUDとはデータベース操作の基本的な4つの機能を表し、「Create(作成)」「Read(読み取り)」「Update(更新)」「Delete(削除)」の頭文字を取っています。
作成するアプリは商品管理アプリ
になります
データベースと連携して商品情報が載った一覧ページを作成したり、一覧から商品を検索したり、商品情報の削除、更新などができるWEBページを作成することが目的になります
知識はGPT4によるチャット形式で必要な情報を引き出し作成を実行しているため2021年9月までの情報をもとに作成しています
ログイン機能について
以下の記事ですでに実装済みなので興味があればこちらを参照ください
環境要件
Javaのバージョン: 17
Spring Bootのバージョン: 2.5.4
MacOS
Eclipse(IDE)
MAMP(MySQL)
プロジェクト名:ProductManagementSystem
プロジェクトを作成と依存関係について
eclipseではプロジェクトを作成する際に機能(依存関係)を追加することができます
今回のCRUDアプリ開発に際し最低限必要な依存関係は以下になります
-
Spring Data JPA
-データベースとJavaのフィールド(変数)を紐付けします -
Spring Web
- CRUD機能を呼び出せます
-
Tyhmeleaf
- HTMLで変数を扱えるようにします
-
Spring Security (ログイン機能を実装する場合に必要)
- セキュリティ機能を向上させます
データベースと接続させる
SpringBootとデータベースを接続するにはapplication.properties
へ接続情報を記述する必要があります
src
└── main
└── resources
└── application.properties
MAMPの場合
MAMPにはMySQLが標準で使用でき、接続情報も公開されています
# MySQLの接続設定
spring.datasource.url=jdbc:mysql://localhost:8889/productmanagementsystem?useSSL=false&serverTimezone=Asia/Tokyo
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPAの設定
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect
接続情報の補足
-
spring.datasource.url
データベースへの接続URLです。ここではlocalhost:8889でMySQLが実行されており、データベース名はproductmanagementsystemとしています。さらにuseSSL=falseはSSL接続を無効にしており、serverTimezone=JSTは日本時間を設定しています。 - spring.datasource.username
-
spring.datasource.password
MySQLのユーザー名とパスワードです。ここでは、両方ともrootを指定しています。 -
spring.datasource.driver-class-name
使用するJDBCドライバーを指定します。MySQLの場合、com.mysql.cj.jdbc.Driverを指定します。 -
spring.jpa.hibernate.ddl-auto=update
アプリケーションが起動するたびにモデルクラスの定義に基づいてテーブルが更新されます。 -
spring.jpa.show-sql=true
Hibernateが発行するSQLをログに出力するように指定します。デバッグやトラブルシューティングに便利です。 -
spring.jpa.properties.hibernate.dialect
使用するデータベースの方言を指定します。ここではMySQL5を指定しています。
これらの設定を記述した後、SpringBootのアプリケーションを起動すれば、Spring Bootは自動的にこの接続情報を使用してMySQLデータベースに接続します。
productmanagementsystemというデータベース名は事前にMySQL上で作成しておく必要があります。
データベースを作成する
データベースを作成するにはSQLで操作を実行するか
phpMyAdminなどのGUIを利用してデータベースを作成する必要があります
MAMPの場合、ターミナルからデータベースに接続するコマンドが以下になります
$ mysql -u root -p -S /Applications/MAMP/tmp/mysql/mysql.sock
CREATE DATABASE productmanagementsystem;
exit
テーブルについてはJavaのコードと連携させるため自動で作成できます
必要なディレクトリを整備する
- モデルクラス(エンティティクラスとも言う)
- テーブルの値とJavaのフィールド(変数)を紐付けるクラス
- リポジトリクラス
- CRUD操作のためのインターフェースを宣言します
- コントローラクラス
- 各CRUD操作のためのURL先を作成します
- サービスクラス
- データベースへの実際の処理内容を記述します
上記の4つのクラスを納めるディレクトリが以下の構造図になるため各ディレクトリを作ります
ProductManagementSystem
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
└── example
└── productmanagementsystem
├── ProductManagementSystemApplication.java
├── controller
│ └── ProductController.java
├── model
│ └── Product.java
├── repository
│ └── ProductRepository.java
└── service
└── ProductService.java
SpringBootを起動する
SpringBootを起動しエラーが出ないことをチェックします
正常にデータベースと接続されていればログにエラーなどが含まれていないことが分かります
エラーが発生した場合は接続情報に誤りがないか確認が必要です
モデルクラスの作成(エンティティとも呼称する)
データベースのテーブルの値と連携させる変数を持つクラスを作成します。このクラスは、商品のid、name、description、priceなどの属性を持つことができ、テーブルの値と紐づくため、データの取得やデータの更新の際などに役立ちます
package com.example.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String description;
private Double price;
}
モデルクラスを作成することで、接続情報がデータベースの自動更新を行う設定になっていれば自動でモデルクラスを参考にテーブルを作成してくれます
リポジトリの作成
データベース操作を抽象化するためのインターフェースです。
インターフェースとは「何をするべきか」をリスト化したものです。
Spring Data JPAを使用すると、基本的なCRUD操作をすぐに利用できます。
具体的なCRUD操作部分はサービスクラスに記述しますが、サービスクラスでProductRepository
を呼び出すことでCRUD操作を実行することができます
package com.example.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.model.Product;
public interface ProductRepository extends JpaRepository<Product, Long> {
}
リポジトリクラスの補足
このコードはSpring Data JPAのリポジトリを定義しています。JpaRepositoryインターフェースは2つの型パラメータを持ちます。
-
Product
エンティティクラスの型を指定します。この例ではProductエンティティに対する操作をこのリポジトリで扱うことを意味します。 -
Long
エンティティの主キー(ID)の型を指定します。この例では、ProductエンティティのIDフィールドがLong型であることを示しています。
つまり、ProductRepositoryインターフェースはProductエンティティに対してLong型のIDを使用するCRUD操作(作成、読み込み、更新、削除)を提供します。
サービスクラスの作成
リポジトリクラスで作成したCRUDを操作するためのインターフェースを呼び出し、コントローラクラスへのデータの流れを調整します。具体的にはデータの取得や保存、変更、削除などの操作への指示はここに書きます。そうすることでこのアプリが何をするのか、規則や手順をひとまとめにすることができます。なおこの手法をカプセル化と言い、オブジェクト指向プログラミングの基本的な概念の一つです。
コードをカプセル化することにより、コードの管理が容易になりプログラムのエラーを防ぐことにも繋がります
package com.example.service;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.model.Product;
import com.example.repository.ProductRepository;
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public Product saveProduct(Product product) {
return productRepository.save(product);
}
@Transactional
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Transactional
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
@Transactional
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
サービスクラスの補足
@Transactional
トランザクション
として扱うことを示すアノテーションです。つまり、そのアノテーションがついているメソッドの中で行われるデータベースへの全ての操作が1つのまとまりとして扱われます。
データベースのトランザクションとは、「全てうまくいくか、全て失敗するか」のどちらか一方を保証する仕組みです。一連のデータ操作の途中で何か問題が発生した場合(例えば、データの保存中にエラーが起きた場合)、そのトランザクション全体が元の状態に戻されます(これをロールバック
といいます)。これにより、データの状態が不整合になるのを防ぎます。
@Service
@Serviceアノテーションは、Springフレームワークで非常に重要な役割を果たします。このアノテーションがついたクラスは、Springによって「サービスクラス」として認識され、アプリが目的を達するためのコード(作成・取得・更新・削除など)を処理します。
@Service以外にも、@Component, @Repository, @Controllerなどが存在し、これらはビーン
と呼ばれSpringBootから「ここで作ったものは、他のところでも使えるようにする」というように認識されるようになります
逆にビーン
を設置しない場合は必要な場所で自動的に使えるようにならないため、リポジトリクラスやモデルクラスなどにも各々ビーンを付与する必要があります
private
アクセス修飾子の一つで、同じクラス内からのみアクセス可能であることを意味します
変数を宣言することでクラスの外部からのアクセスを制限し、意図しない値を代入されることや不正な操作を防ぐことができます
サービスクラスでリポジトリクラスのインターフェースを使えるようにするために、リポジトリクラスのインスタンスを宣言しています。
ProductRepository
クラスの型を示しています。ProductRepository
は商品データをデータベースとやり取りするために作成されたオリジナルのクラスです。
productRepository
ProductRepositoryクラスのインスタンス(オブジェクト)を表しています。具体的なデータベース操作を行うために、ProductRepositoryクラスのインスタンスがこの変数に格納されます。この変数を介して、サービスクラス(ProductService)はデータベースとのやり取りを行います。
つまりProductService
クラス内でproductRepository
という変数を使用して、ProductRepository
クラスのメソッドを呼び出します。これによりProductService
クラスはProductRepository
クラスの機能を利用して、データベースとのやり取りを行うことができます。
private ProductRepository productRepository;
// ↑クラス名 ↑変数名
@Autowired
前述のprivate ProductRepository productRepository;
のようにProductRepository
というインターフェースを単純にフィールドとして宣言しただけではそのクラスのインスタンスが生成されるわけではなく使うことができません。
以下のようにコンストラクタを書き依存性注入をすることで初めてインスタンスを利用することができます
@Autowired
// アノテーションを付与することで
// 以下を省略できますが、記述することで依存性が明示的になり
// テストが容易になり、また不変性が保証されるため記述しておくことが推奨されています
// public ProductService(ProductRepository productRepository) {
// this.productRepository = productRepository;
// }
save****()
-
saveProduct
保存を意味するsaveとProductテーブルを組み合わせた任意のメソッド名です -
public Product
publicは他のクラスからアクセスを許可することができます。serviceクラス
はcontrollerクラス
から呼び出されるのでアクセスの許可が必要になります
Produce
は戻り値の型を指し、戻り値がProductテーブルに関する値だということを示します -
引数 (Product product)について
Productクラスのインスタンスをproductという変数で受け取ることができます -
return productRepository.save(product);
productRepositoryというインターフェースを呼び出し、CRUD処理のsaveプロパティをproduct
モデルと紐づいたproducts
テーブルに保存した状態を戻り値として返しています
List<***>
-
List
List型、つまり複数の要素を順序付けして保持した戻り値を宣言しており、中身はProduct
テーブルに関する情報であることを示しています。名称につきましてもProduct.java
というモデルクラスと関連づけさせる必要があるので任意名というわけではありません -
getAllProducts()
任意のメソッド名になります。Productsテーブルの全てのデータを取得する意味
から命名されています
public Product getProductById(Long id)
-
引数(Long id) について
テーブルの主キーにLong型のidというカラムが設定されておりそれを引数として渡しています -
productRepository.findByID(id)
CRUDを操作するインターフェースを使用し該当するid
のレコードを取得します。 -
.orElse(null);
さらに取得した結果が見つからなかった場合はnull
を返します。これにより該当するレコードが見つからなかったことを指すことができます
productRepository.deleteById(id);
引数で渡されたID
でproductsテーブルを検索し該当するIDを削除します。戻り値は必要ないのでvoid(空)で戻り値が存在しないことを示しており、returnもありません
コントローラークラスの作成
ユーザーからのリクエスト(URLにアクセス)がコントローラーに来たとき、そのリクエストに対する適切な処理(例えば、データの取得や保存)が必要な場合、コントローラーはその仕事をサービスクラスに依頼します。サービスクラスは必要な処理をする役割を果たします。つまり、「ユーザーのリクエストに対して何をするべきか」を決定します。
サービスクラスが具体的なデータ操作(データベースからのデータの取得、データの更新など)を必要とした場合、リポジトリにその仕事を依頼します。リポジトリはデータベースの操作を担当します。
ユーザーからの要求をコントローラーが受け取り、シンプル(静的)なHTMLを表示させるだけならそのページに移動させますが、それがデータベースの情報を必要とするものであればサービスクラスに依頼し、サービスクラスは必要に応じてリポジトリにデータ操作を依頼します。このように各クラスが連携して動作し、全体として一つのWebアプリケーションが動作します。
// 商品情報を操作するためのコントローラを作ります。このクラスはWeb上の操作を受け付けて、それに応じて処理を行う役割があります。
package com.example.controller;
import java.util.List;
// Springフレームワークから必要なクラスをインポートします。これらのクラスがWeb操作とそれに応じた処理をつなげる役割を果たします。
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.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
// 必要なクラスをインポート(持ってきて)います。このクラスは商品情報を表します。
import com.example.model.Product;
// 商品情報の操作を行うサービスをインポートします。このサービスクラスが実際の操作(保存、取得、削除)を行います。
import com.example.service.ProductService;
// '@Controller'と書いてあるのは、このクラスがWebからの操作を受け付ける役割を果たすという意味です。
@Controller
public class ProductController {
// 商品操作サービスのインスタンス(実体)を作ります。このサービスが実際の商品操作を行います。
private final ProductService productService;
// '@Autowired'と書いてあるのは、この下に書かれている部分(商品操作サービス)が自動的に準備されるという意味です。
// このクラス内のどこからでも、この商品操作サービスを利用できるようになります。
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
// Web上で'/'(ホームページ)にアクセスしたときの処理を書きます。
@GetMapping("/")
public String viewHomePage(Model model) {
// 商品操作サービスから全ての商品情報を取得します。
List<Product> listProducts = productService.getAllProducts();
// 取得した商品情報を画面(ビュー)に渡します。画面上でこの情報を利用できるようになります。
model.addAttribute("listProducts", listProducts);
// 表示する画面(ビュー)の名前を返します。この場合、'index'という名前の画面を表示します。
return "index";
}
// 商品を新しく作るための画面にアクセスしたときの処理を書きます。
@GetMapping("/showNewProductForm")
public String showNewProductForm(Model model) {
// 新しい商品情報を作ります。
Product product = new Product();
// 新しい商品情報を画面(ビュー)に渡します。
model.addAttribute("product", product);
// 商品を新しく作る画面('create')を表示します。
return "create";
}
// Web上で新しい商品情報を送信したとき(保存ボタンを押したとき)の処理を書きます。
@PostMapping("/saveProduct")
public String saveProduct(@ModelAttribute("product") Product product) {
// 商品操作サービスを使って、新しい商品情報を保存します。
productService.saveProduct(product);
// 保存が完了したら、ホームページに戻ります。
return "redirect:/";
}
// 商品情報を更新するための画面にアクセスしたときの処理を書きます。
@GetMapping("/showFormForUpdate/{id}")
public String showFormForUpdate(@PathVariable(value = "id") long id, Model model) {
// 更新する商品の情報を取得します。
Product product = productService.getProductById(id).orElse(null);
// 取得した商品情報を画面(ビュー)に渡します。
model.addAttribute("product", product);
// 商品情報を更新する画面('edit')を表示します。
return "edit";
}
// 商品情報を削除するときの処理を書きます。
@GetMapping("/deleteProduct/{id}")
public String deleteProduct(@PathVariable(value = "id") long id) {
// 商品操作サービスを使って、指定された商品情報を削除します。
this.productService.deleteProduct(id);
// 削除が完了したら、ホームページに戻ります。
return "redirect:/";
}
// 商品の詳細情報を表示する画面にアクセスしたときの処理を書きます。
@GetMapping("/productDetail/{id}")
public String productDetail(@PathVariable(value = "id") long id, Model model) {
// 詳細を表示する商品の情報を取得します。
Product product = productService.getProductById(id).orElse(null);
// 取得した商品情報を画面(ビュー)に渡します。
model.addAttribute("product", product);
// 商品の詳細情報を表示する画面('detail')を表示します。
return "detail";
}
}
-
POST /api/products/add
新規商品を追加します。リクエストボディには新規商品の詳細を含めます。 -
GET /api/products
すべての商品のリストを取得します。 -
GET /api/products/{id}
指定されたIDの商品を取得します。 -
DELETE /api/products/{id}
指定されたIDの商品を削除します。
コントローラークラスの補足
@RestController
SpringBootのシステムにこのファイルがcontroller
クラスであることを認識させるためのアノテーションです。いわばマーク
のようなもので、このマークがついたクラスはインターネットのやり取りで道案内を担当できます
@Autowired
「@Autowired」アノテーションを使えば必要なクラスのインスタンスを自動的に注入してくれます。
@Autowired
// サービスクラスを使えるようにインスタンス化している
private ProductService productService;
// インスタンス変数を使用するための依存性注入コードを省略できます
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
引数の(Model model)について
// Web上で'/'(ホームページ)にアクセスしたときの処理を書きます。
@GetMapping("/")
public String viewHomePage(Model model) {
// 商品操作サービスから全ての商品情報を取得します。
List<Product> listProducts = productService.getAllProducts();
// 取得した商品情報を画面(ビュー)に渡します。画面上でこの情報を利用できるようになります。
model.addAttribute("listProducts", listProducts);
// 表示する画面(ビュー)の名前を返します。この場合、'index'という名前の画面を表示します。
return "index";
}
ビュー(HTML)に対してデータベースのデータを送るためにはModelインターフェース
が必要ですが使用するためにはビューを表示させるメソッドに対して引数としてModelを変数に代入
を送る必要があり、引数として渡さなければControllerからViewへデータを渡すこの便利な機能を利用することはできません。
List listProducts について
List<Product> listProducts = productService.getAllProducts();
「List listProducts」というコードの部分で新しいリストを作ります。リストとは、いくつかのデータを並べて管理するためのもので、この場合は「商品」を並べて管理するためのリストを作ります。そしてそのリストの名前は「listProducts」です。
「= productService.getAllProducts();」という部分は、このリストに何を入れるかを決めています。「productService.getAllProducts()」という部分は、「productService」という名前のクラスを使って、getAllProducts()メソッドを呼び出し「すべての商品」を取得する、という意味です。
つまり、「List listProducts = productService.getAllProducts();」という全体のコードは、「すべての商品を取得して、それを「listProducts」という名前のリストに入れてください」という意味になります。
このリストは後で、画面にすべての商品情報を表示するために使われます。
引数の(@RequestBody Product product) について
@PostMapping("/add")
public Product addProduct(@RequestBody Product product) {
return productService.saveProduct(product);
}
ウェブサイトから送られてくる情報(この場合は、新しい商品の情報)を読み取り、その情報を使ってproduct
という名前の新しい引数を作るためのアノテーションです
例えば、ウェブサイトから以下のような情報が送られてきたとします
{
"name": "新しい商品",
"description": "これは新しい商品の説明です",
"price": 1000
}
Product
はモデル名(javaファイル)を指しており、引数として渡すためにproduct
に代入します。
注意点として、送られた情報のフィールド名と、モデルクラスのフィールド名(変数名)が相違する場合はエラーが発生します
return productService.saveProduct(product);
productService
というインスタンスからsaveProduct
メソッドを呼び出し引数を渡すことで関連するテーブルに情報を保存させます
戻り値として、保存操作が完了した後のモデル情報を参照し、必要であればHTMLの一覧画面を更新することができます
@GetMapping
本来ならば@GetMapping("/{id}")
のようにパスを指定しますが
@RequestMapping("/api/products")
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
このように@RequestMapping
が存在する場合は
/api/products のGET通信を指定された場合に処理が実行されることになります
またGETメソッドはデータベースからレコードを取得する場合などによく使用されます
引数の(@PathVariable Long id) について
@PathVariableは、URLの一部をメソッドの引数として利用するためのアノテーションです。具体的には、URLの中に含まれる「パス変数」をメソッドの引数に直接マッピングします。
たとえば、URLが"/api/products/123"のようになっていて、"123"という部分が商品のIDであるとします。このIDをメソッドの引数として取り出したい場合、次のように書きます
@GetMapping("/{id}")
public Product getProductById(@PathVariable Long id) {
return productService.getProductById(id);
}
@PathVariableアノテーションが付いたLong idという引数は、URLの"/{id}"部分に相当します。つまり"/api/products/123"というURLに対するGETリクエストがあった場合、このメソッドはproductServiceのgetProductByIdメソッドを呼び出して、IDが123の商品を取得します。
@DeleteMapping
HTTPのDELETEリクエストに対して特定のメソッドを実行するよう指定するためのマーカーです。特定のリソースを削除するために使用されます。
GETやPOSTメソッドを使ってリソースを削除することも技術的には可能ですが、それらのメソッドの本来の目的とは異なるため、一般的には推奨されません。
フロントエンド(HTMLを作成する)
商品管理システムを作成するにあたりまして、以下の基本的な画面構成を予定しています
- 製品一覧画面(index.html)
- 製品詳細画面(detail.html)
- 製品追加画面(create.html)
- 製品編集画面(edit.html)
ファイル構造図(HTML)
YourProject/
│
├── src/
│ ├── main/
│ │ ├──
│ │ ├── resources/
│ │ │ ├── templates/
│ │ │ │ ├── index.html
│ │ │ │ ├── create.html
│ │ │ │ ├── detail.html
│ │ │ │ ├── edit.html
│ │ │ │ └── ...
│ │ │ ├── application.properties
サンプルコード(HTML)
<!-- HTML文書の開始を表すタグです -->
<!DOCTYPE html>
<!-- html要素でHTML文書全体を囲む。xmlns属性はThymeleafというライブラリを使うためのもの -->
<html xmlns:th="http://www.thymeleaf.org">
<!-- head要素内には文書に関する情報が入ります。今回はページのタイトルだけです -->
<head>
<!-- ページのタイトルを設定します -->
<title>商品管理一覧ページ</title>
</head>
<!-- body要素内には表示する内容が入ります -->
<body>
<!-- 製品一覧の見出し -->
<h1>製品一覧ページ</h1>
<!-- 製品追加ページへのリンク -->
<a href="/showNewProductForm">新しい商品を登録する</a>
<!-- 表(table)を作成します。表は行(tr)とセル(td)で構成されます -->
<table>
<!-- 表の見出し部分 -->
<thead>
<tr>
<th>商品名</th>
<th>説明(コメント)</th>
<th>操作</th>
</tr>
</thead>
<!-- 表の本体部分。各製品に対して行を作成します -->
<tbody>
<!-- listProductsから1つずつ製品を取り出し、それに対する行を作成します -->
<tr th:each="product : ${listProducts}">
<!-- 製品の名前を表示します -->
<td th:text="${product.name}"></td>
<!-- 製品の説明を表示します -->
<td th:text="${product.description}"></td>
<td>
<!-- 更新ページへのリンク。URLに製品のIDを含めます -->
<a th:href="@{/showFormForUpdate/{id}(id=${product.id})}">詳細</a>
<!-- 削除を実行するリンク。URLに製品のIDを含めます -->
<a th:href="@{/deleteProduct/{id}(id=${product.id})}">削除</a>
</td>
</tr>
</tbody>
</table>
</body>
</html>
<!-- HTML文書の開始を表すタグです -->
<!DOCTYPE html>
<!-- html要素でHTML文書全体を囲む。xmlns属性はThymeleafというライブラリを使うためのもの -->
<html xmlns:th="http://www.thymeleaf.org">
<!-- head要素内には文書に関する情報が入ります。今回はページのタイトルだけです -->
<head>
<!-- ページのタイトルを設定します -->
<title>商品編集ページ</title>
</head>
<!-- body要素内には表示する内容が入ります -->
<body>
<!-- 製品編集の見出し -->
<h1>商品編集ページ</h1>
<!-- 製品情報を編集するためのフォーム -->
<form action="#" th:action="@{/saveProduct}" th:object="${product}" method="post">
<!-- 製品IDは編集しないので、非表示の入力欄で送信します -->
<input type="hidden" th:field="*{id}">
<!-- ラベルと入力欄(input要素)の組を作ります -->
<label for="name">商品名</label>
<input type="text" th:field="*{name}" id="name">
<!-- ラベルと入力欄(input要素)の組を作ります -->
<label for="description">説明(コメント)</label>
<input type="text" th:field="*{description}" id="description">
<!-- 送信ボタン -->
<input type="submit" value="更新する">
</form>
<!-- 製品一覧ページへのリンク -->
<a href="/">一覧ページに戻る</a>
</body>
</html>
<!-- HTML文書の開始を表すタグです -->
<!DOCTYPE html>
<!-- html要素でHTML文書全体を囲む。xmlns属性はThymeleafというライブラリを使うためのもの -->
<html xmlns:th="http://www.thymeleaf.org">
<!-- head要素内には文書に関する情報が入ります。今回はページのタイトルだけです -->
<head>
<!-- ページのタイトルを設定します -->
<title>新しい商品登録ページ</title>
</head>
<!-- body要素内には表示する内容が入ります -->
<body>
<!-- 製品追加の見出し -->
<h1>新しい商品登録ページ</h1>
<!-- 製品情報を入力するためのフォーム -->
<form action="#" th:action="@{/saveProduct}" th:object="${product}" method="post">
<!-- ラベルと入力欄(input要素)の組を作ります -->
<label for="name">商品名</label>
<input type="text" th:field="*{name}" id="name">
<!-- ラベルと入力欄(input要素)の組を作ります -->
<label for="description">説明(コメント)</label>
<input type="text" th:field="*{description}" id="description">
<!-- 送信ボタン -->
<input type="submit" value="登録する">
</form>
<!-- 製品一覧ページへのリンク -->
<a href="/">一覧ページに戻る</a>
</body>
</html>
<!-- HTML文書の開始を表すタグです -->
<!DOCTYPE html>
<!-- html要素でHTML文書全体を囲む。xmlns属性はThymeleafというライブラリを使うためのもの -->
<html xmlns:th="http://www.thymeleaf.org">
<!-- head要素内には文書に関する情報が入ります。今回はページのタイトルだけです -->
<head>
<!-- ページのタイトルを設定します -->
<title>商品編集ページ</title>
</head>
<!-- body要素内には表示する内容が入ります -->
<body>
<!-- 製品編集の見出し -->
<h1>商品編集ページ</h1>
<!-- 製品情報を編集するためのフォーム -->
<form action="#" th:action="@{/saveProduct}" th:object="${product}" method="post">
<!-- 製品IDは編集しないので、非表示の入力欄で送信します -->
<input type="hidden" th:field="*{id}">
<!-- ラベルと入力欄(input要素)の組を作ります -->
<label for="name">商品名</label>
<input type="text" th:field="*{name}" id="name">
<!-- ラベルと入力欄(input要素)の組を作ります -->
<label for="description">説明(コメント)</label>
<input type="text" th:field="*{description}" id="description">
<!-- 送信ボタン -->
<input type="submit" value="更新する">
</form>
<!-- 製品一覧ページへのリンク -->
<a href="/">一覧ページに戻る</a>
</body>
</html>
HTML(Thymeleaf)の補足
Thymeleafとはサーバーで作られたデータをHTMLで表示させるツールの一つです
xmlns:th="http://www.thymeleaf.org"
// XMLネームスペース宣言で、ドキュメント内のThymeleafを使用することを示しており
// 宣言することでSpringBootが以下のthymeleafコードを認識できるようになります
th:action="@{/saveProduct}"
// フォームが送信されたときにリクエストを送信するURLを指定します。
// /saveProductというURLにリクエストが送信されます。
// SpringSecurity(ログイン機能)を導入している場合、
// CSRFトークンを一緒に送信しないと通信がブロックされ処理が中断されます
// @{...} 構文を使うとCSRFトークンが一緒に送信されるため
// SpringSecurityを依存関係に加えている場合もフォームを使用できます
th:object="${product}"
// 送信先のコントローラーと関連付けするための名前です
// コントローラーの引数には @ModelAttribute("product") と記述されることで
// フォームの"product"が関連付けされ、さらにProductモデルと関連付けされます
//フォームはproductという名前のオブジェクトを操作することになり
// Productsテーブルの情報を操作することになります
// 名称は一般的に小文字が推奨されており
// 大文字小文字の差異があってもモデルクラスと関連付けができます
th:field="*{id}"
// サーバーサイドから送られたレコードの情報をHTMLで表示させるコードです
// "*{id}" この部分にテーブルのカラムの名前を記述することで
// そのカラムの値を表示させることができ
// またinputなどで入力した情報をサーバーに送るために
// 格納することができます
th:each="product : ${listProducts}"
// リストや配列の各要素に対して反復処理を行います
// listProductsという名前のリストの各要素に対して反復処理が行われます。
th:text="${product.name}"
// サーバーサイドから送られたProductsテーブルのname列の値を表示させます
th:href="@{/showFormForUpdate/{id}(id=${product.id})}"
// @{/showFormForUpdate/{id} は、リンク先のページの名前(URL)を指定しています
// (id=${product.id}) は、上記の {id} の部分に何を入れるべきかを教えており
// つまり、 "リンクのURLに商品のID番号を入れる"よう指示が入っています
// (id=${product.id}) を省略した場合
// /showFormForUpdate/{id} のままになります
検索機能を追加する
上記で基本的なCRUD機能は満たしていますが、次のコードで一覧画面に検索フォームを追加していきます
<!--追加コード-->
<form th:action="@{/search}" method="get">
<input type="text" name="searchQuery" placeholder="検索キーワードを入力して下さい">
<button type="submit">Search</button>
</form>
<!--
th:action="@{/search}"でCSRF対策(トークンを付与)をしながらデータを送信します
th:object="${product}"でProductモデルと関連付けます
関連付けする名称はモデル名では最初が大文字でしたが、一般的に全て小文字がになります
name="searchQuery"でコントローラーの@RequestParam("searchQuery")と関連付けます
-->
// 追加コード
@GetMapping("/search")
public String search(@RequestParam("searchQuery") String searchQuery, Model model) {
List<Product> listProducts = productService.searchProducts(searchQuery);
model.addAttribute("listProducts", listProducts);
return "index";
}
// フォームから渡された"searchQuery"を
// @RequestParam("searchQuery")で引数として渡します
// listProductsという変数を用意してサーチ結果を代入します
// サーチ結果はproductServiceクラスからsearchProductsを使って検索します
// modelを使ってindex.htmlで変数を使えるようにします
// 追加コード
@Transactional
public List<Product> searchProducts(String searchQuery) {
return productRepository.findByNameContaining(searchQuery);
}
// productRepositoryクラスから
// 新しく設置したfindByNameContainingメソッドを呼び出し
// 引数としてsearchQueryを渡します
## GitHub
https://github.com/AlecTrippier/ProductManagementSystem
// コード全部書き換え
package com.example.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.model.Product;
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String searchQuery);
}
// findByNameContaining(String searchQuery);
// JPAのメソッド名によるクエリ生成機能を利用したものです
// 渡された引数の文字列(searchQuery)を含む」全ての
// エンティティを検索するためのクエリを生成します。
// 生成されたクエリは、その名前が searchQuery を含むすべてのレコードを返します
サンプル画像
GitHub