今回は以前の記事で構築したSpringBootプロジェクトを使って簡単なCRUD操作ができる画面を作成してみたいと思います。
環境構築がまだの人は以下を参考にしてください。
今回作成する画面
今回作成する画面は、単純にユーザー情報を一覧表示/登録/変更/削除ができるいたってシンプルな画面です。
ユーザー一覧画面
現在登録されているユーザー一覧が表示される画面です。右上にユーザーの新規登録ボタン、各データごとに編集と削除ボタンを設置しています。
新規登録ボタンを押すと新規登録画面へ編集ボタンを押すとユーザー編集画面に遷移します。
削除ボタンを押すと選択されたユーザーを物理削除します。
新規登録画面
ユーザー一覧の右上の新規登録ボタンをクリックすると表示される画面です。
登録情報を入力後登録ボタンを押すとユーザー情報が登録されユーザー一覧画面に戻ります。
戻るボタンを押してもユーザー一覧画面に戻ります。
登録時に簡単なバリデーションチェックを行い、エラーがあれば画面に表示するようにしています。
ユーザー編集画面
登録されているユーザーごとに設置されている編集ボタンを押すと表示される画面です。
確定ボタンを押すとデータベースに変更したユーザー情報が反映されます。新規登録画面と同様のバリデーションチェックも備えています。
CRUD操作は一通り網羅出来ていると思います。
開発環境
開発環境 | 名称 | 説明 |
---|---|---|
言語 | Java | java 17.0.5 2022-10-18 LTS |
開発ツール | VSCode | Visual Studio Code |
フレームワーク | Spring Boot | バージョン3.0.2 |
データベース | SQL Server | Microsoft SQL Server 2019 Express |
ビルドツール | Gradle | バージョン7.6 |
テンプレートエンジン | Thymeleaf (タイムリーフ) | Spring BootでWeb画面を作成するときによく使われるテンプレートエンジン |
ORMマッパー | MyBatis | Javaでよく使用されているORマッパー |
CSSフレームワーク | Bootstrap | バージョン5.02 ※今回はCDN形式で使用 |
今回構築した環境はSpringBoot3.02で構築しています。
3.02は、Spring Framework 6が採用されており、バージョン2の時とは微妙に書き方が変わります。私が一番はまったところは、Validation(バリデーション)機能を使うためにjavax.validation.constraintsをインポートしようとしたところインポートできませんでした。
Spring Framework6からは、Jakartaに移行されているみたいなのでjakarta.validation.constraintsでインポートする必要があるみたいです。
フォルダ構成
フォルダ構成は悩みましたが以下のようにしています。
各フォルダごとの役割
フォルダ名 | 役割 |
---|---|
cotroller | 画面からの要求を受け取りサービスクラスに処理を依頼する。 処理結果が返ってきたら指定した画面に結果を返す役割。 |
dao | サービスクラスの処理でデータベースにアクセスする場合に使用する。 今回はMyBatisがこの役割を担っていてinterfaceで実装されている。 |
dto | daoで受け取ったデータをサービスに受け渡すためのクラス。 ここでは、受け取ったデータのバリデーションチェックで利用している。 |
entity | データベースのテーブル定義をJavaに認識させるためのクラス。 基本的に一つのテーブルに対して一つのEntityクラスが存在し、テーブル定義が記述される。 |
service | controllerから依頼された処理を行い、contorollerクラスに結果を返す役割。 基本的にここに実処理を記載する。 データベースに接続する際はdao経由でデータを取得する。 |
MyBatisのマッピングファイル(xml)の配置場所
マッピングファイル(xml)ファイルは基本的にresourceフォルダにjavaと同じ階層を作成する必要があります。同じ階層に格納することででMybatisが自動的に認識してくれます。今回でいうと以下のようになります。
- src\main\java\com\example\demo\dao\UserDao.java(マッパークラス)
- src\main\resources\com\example\demo\dao\UserDao.xml(xmlファイル)
※application.propertiesに設定すればに任意の場所に格納することも可能
htmlファイル(テンプレート)の配置場所
WEB画面のHTMLファイルは、src\main\resources\templates配下に格納します。
このフォルダに格納すれば、Thymeleafがテンプレートファイルを自動で検索してくれます。
テーブル定義
今回はSQL Serverを使用しています。事前にSQLServerに以下のデータベースとテーブルを作成しておきます。
データベース名:sample
テーブル名:users
列名 | データ型 | 説明 |
---|---|---|
userid | varchar(24) | ユーザーのID(主キー) |
name | varchar(50) | ユーザーの名前 |
varchar(255) | メールアドレス |
SQL Serverで実行したSQL
USE [sample]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[users](
[userid] [varchar](24) NOT NULL,
[name] [varchar](50) NOT NULL,
[email] [varchar](255) NOT NULL,
CONSTRAINT [PK_users_1] PRIMARY KEY CLUSTERED
(
[userid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
application.properties
application.propertiesは、プロジェクト作成時に自動で作成されています。
作成先:src\main\resources\application.properties
springbootを起動させるポートの設定とDBの接続情報を記載しています。
server.port = 8091
# DB接続情報
spring.datasource.url=jdbc:sqlserver://localhost:1433;Database=sample;integratedSecurity=false;encrypt=true;trustServerCertificate=true;
spring.datasource.username=sa
spring.datasource.password=xxxxxx
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
# SQLSQLログ出力
logging.level.org.springframework=warn
logging.level.com.example.demo.UserDataMapper=debug
build.gradle
build.gradleは以下のように設定しています。
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.0'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
バックエンド側の実装
DemoApplication.java
DemoApplication.javaはプロジェクト作成時に自動で作成されたクラスファイルです。
SpringApplication.runでspringbootを起動するファイルです。今回は特に変更は行いません。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Contoroller(UserController.java)
まずはコントローラーを作成します。ソースをコピペした場合は、なみなみでエラーになりますがすべてのソースを配置するとエラーは消えると思います。
このクラスはコントローラーですよ!とSpringBootに認識してもらうために@Controllerを付与します。
基本的には、画面からリクエストされたデータをサービスクラスに処理を依頼し、結果をテンプレートに返す役割をしています。returnで返しているのはtemplateフォルダ配下のhtmlファイルになっています。redirect /は初期画面に設定しているユーザー一覧画面にリダイレクトしています。
package com.example.demo.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.example.demo.service.UserService;
import com.example.demo.dto.UserAddRequest;
import com.example.demo.dto.UserUpdateRequest;
import com.example.demo.entity.User;
/**
* ユーザー Controller
*/
@Controller
public class UserController {
/**
* UserServiceを使用できるようにする
*/
@Autowired
private UserService userService;
/**
* ユーザーリストをuserlist.htmlに渡す。
*
* @param model Model
* @return ユーザー一覧画面
*/
@GetMapping(value="/")
public String viewList(Model model){
List<User> userList = userService.findAll();
model.addAttribute("userlist", userList);
return "user/userlist";
}
/**
* ユーザー新規登録画面に遷移
* @param model Model
* @return ユーザー新規登録画面
*/
@GetMapping(value = "/user/add")
public String displayAdd(Model model) {
model.addAttribute("userAddRequest", new UserAddRequest());
return "user/useradd";
}
/**
* ユーザー新規登録
* @param userRequest リクエストデータ
* @param model Model
* @return ユーザー情報一覧画面
*/
@RequestMapping(value = "/user/create", method = RequestMethod.POST)
public String create(@Validated @ModelAttribute UserAddRequest userRequest, BindingResult result, Model model) {
if (result.hasErrors()) {
// Validationエラーの場合はメッセージを表示
List<String> errorList = new ArrayList<String>();
for (ObjectError error : result.getAllErrors()) {
errorList.add(error.getDefaultMessage());
}
model.addAttribute("validationError", errorList);
return "user/useradd";
}
// 登録したらユーザー一覧を表示
userService.save(userRequest);
return "redirect:/";
}
/**
* ユーザー削除(物理削除)
* @param userid ユーザーID
* @param model Model
* @return ユーザー情報一覧画面
*/
@GetMapping("/user/{userid}/delete")
public String delete(@PathVariable String userid, Model model) {
// ユーザー情報の削除
userService.delete(userid);
return "redirect:/";
}
/**
* ユーザー編集画面を表示
* @param userid ユーザーID
* @param model Model
* @return ユーザー編集画面
*/
@GetMapping("/user/{userid}/edit")
public String viewEdit(@PathVariable String userid, Model model) {
User user = userService.findById(userid);
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid(user.getUserid());
userUpdateRequest.setName(user.getName());
userUpdateRequest.setEmail(user.getEmail());
model.addAttribute("userUpdateRequest", userUpdateRequest);
return "user/useredit";
}
/**
* ユーザー更新
* @param userRequest リクエストデータ
* @param model Model
* @return ユーザー編集画面
*/
@RequestMapping(value = "/user/update", method = RequestMethod.POST)
public String update(@Validated @ModelAttribute UserUpdateRequest userUpdateRequest, BindingResult result, Model model) {
if (result.hasErrors()) {
List<String> errorList = new ArrayList<String>();
for (ObjectError error : result.getAllErrors()) {
errorList.add(error.getDefaultMessage());
}
model.addAttribute("validationError", errorList);
return "user/useredit";
}
// 編集したらユーザー一覧画面に戻る
userService.update(userUpdateRequest);
return "redirect:/";
}
}
Service(UserService.java)
次にサービスクラスを実装します。このクラスがサービスクラスと認識させるために@Serviceを付与しています。基本はここに実処理を記述していきます。今回はデータベースからユーザー情報を取得するだけのなのでDaoを呼び出しています。
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import com.example.demo.dao.UserDao;
import com.example.demo.entity.User;
import com.example.demo.dto.UserAddRequest;
import com.example.demo.dto.UserUpdateRequest;
@Service
public class UserService {
@Autowired
private UserDao userDao;
/**
* ユーザ全検索
* @param userAddRequest リクエストデータ
*/
public List<User> findAll(){
return userDao.findAll();
}
/**
* ユーザ新規登録
* @param userAddRequest リクエストデータ
*/
public void save(UserAddRequest userAddRequest) {
userDao.save(userAddRequest);
}
/**
* ユーザー物理削除
* @param userid
*/
public void delete(String userid) {
userDao.delete(userid);
}
/**
* ユーザーID検索
* @return 検索結果
*/
public User findById(String userid) {
return userDao.findById(userid);
}
/**
* ユーザ情報更新
* @param userEditRequest リクエストデータ
*/
public void update(UserUpdateRequest userUpdateRequest) {
userDao.update(userUpdateRequest);
}
}
Dao(UserDao.java)
続いてDaoを記述していきます。今回はORマッパーでMyBatisを使用しているので@Mapperを付与しています。このDaoはinterfaceになっており、MyBatisがこのinterfaceをマッピングしてくれます。
package com.example.demo.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.demo.entity.User;
import com.example.demo.dto.UserAddRequest;
import com.example.demo.dto.UserUpdateRequest;
@Mapper
public interface UserDao {
/**
* リスト型でユーザー一覧を返す
*/
List<User> findAll();
/**
* ユーザー新規登録
* @param userRequest (新規登録用リクエストデータ)
*/
void save(UserAddRequest userRequest);
/**
* ユーザーID検索
* @param userid
*/
User findById(String userid);
/**
* ユーザー情報更新
* @param userUpdateRequest 更新用リクエストデータ
*/
void update(UserUpdateRequest userUpdateRequest);
/**
* ユーザーの物理削除
* @param userid ユーザーID
*/
void delete(String userid);
}
UserDao.xml
DaoとEntityの関連付けをしているファイルです。main\resources\com\example\demo\dao\UserDao.xmlに配置しています。
Mybatisはresourceフォルダの同じ階層のxmlフォルダをデフォルトで見てくれるのでここに配置しています。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserDao">
<!-- 一覧表示 -->
<select id="findAll" resultType="com.example.demo.entity.User">
SELECT userid,name,email FROM users
</select>
<!-- ユーザーID検索 -->
<select id="findById" resultType="com.example.demo.entity.User">
SELECT * FROM users WHERE userid = #{userid}
</select>
<!-- 新規登録 -->
<insert id="save">
INSERT INTO users
(userid, name, email)
VALUES
(#{userid}, #{name}, #{email})
</insert>
<!-- 削除 -->
<delete id="delete">
DELETE FROM users WHERE userid = #{userid}
</delete>
<!-- 更新 -->
<update id="update">
UPDATE users SET name = #{name}, email = #{email} WHERE userid = #{userid}
</update>
</mapper>
Entity(User.java)
データベースのテーブル定義をJavaに認識させるためクラスです。基本的にテーブル定義を記載していきます。今回のテーブル定義はユーザーテーブルですべて文字列の型なのでStringとしています。
package com.example.demo.entity;
import java.io.Serializable;
import lombok.Data;
@Data
public class User implements Serializable {
//ユーザーID
private String userid;
//名前
private String name;
//メールアドレス
private String email;
}
Dto(UserAddRequest.java/UserUpdateRequest.java)
Daoで取得したデータのバリデーションチェックを行っているクラスです。UserAddRequest.javaは新規登録用でUserUpdateRequest.javaは編集用です。今回は新規登録時と同じバリデーションチェックを行うので新規登録用の処理を継承(extends)して使用しています。
package com.example.demo.dto;
import java.io.Serializable;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Email;
import lombok.Data;
/**
* ユーザー情報登録 リクエストデータ
*/
@Data
public class UserAddRequest implements Serializable {
/**
* ユーザーID
*/
@NotEmpty(message = "ユーザーIDを入力してください(必須")
@Size(max = 24, message = "名前は24桁以内で入力してください")
private String userid;
/**
* メールアドレス
*/
@NotEmpty(message = "名前を入力してください(必須")
@Size(max = 50, message = "名前は50桁以内で入力してください")
private String name;
/**
* 名前
*/
@NotEmpty(message = "メールアドレスを入力してください(必須")
@Email(message = "メールアドレスの形式で入力してください")
private String email;
}
package com.example.demo.dto;
import lombok.EqualsAndHashCode;
import lombok.Data;
/**
* バリデーションチェックは新規登録用を使う
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserUpdateRequest extends UserAddRequest {
}
フロントエンド側の実装
フロントエンド側はTymeleafというテンプレートエンジン経由でhtmlファイルにバックエンドで取得したデータをはめ込む形になっています。またデザインのボタン部分を多少きれいにしたかったので
CSSフレームワークにBootstrapを採用して共通フォルダにheader.htmlを作成し呼び出すようにしています。(src\main\resources\templates)
common/head.html
共通でBootstrap5.02を使用するようにしています。
<head th:fragment="head_fragment(title, scripts, links)">
<title th:text="${title}"></title>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<meta charset="utf-8" />
</head>
user/useradd.html
新規追加画面用です。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="common/head :: head_fragment(title = 'ユーザー新規登録', scripts = ~{::script}, links = ~{::link})"></head>
<body>
<div class="container">
<div th:if="${validationError}" th:each="error : ${validationError}">
<label class="text-danger" th:text="${error}"></label>
</div>
<h1>ユーザー新規登録</h1>
<form th:action="@{/user/create}" th:object="${userAddRequest}" th:method="post">
<div class="form-group">
<label>ユーザーID : <span class="text-danger">※</span></label>
<input style="width:300px;" type="text" th:field="*{userid}" class="form-control">
</div>
<div class="form-group">
<label>名前:<span class="text-danger">※</span></label>
<input style="width:300px;" type="text" th:field="*{name}" class="form-control">
</div>
<div class="form-group">
<label>メールアドレス : <span class="text-danger">※</span></label>
<input style="width:500px;" type="text" th:field="*{email}" class="form-control">
</div>
<br />
<div class="text-left">
<input type="submit" value="登録" class="btn btn-primary">
<a href="/" class="btn btn-secondary">戻る</a>
</div>
</form>
</div>
</body>
</html>
user/useredit.html
ユーザー編集用画面です。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head
th:replace="common/head :: head_fragment(title = 'ユーザー編集', scripts = ~{::script}, links = ~{::link})"></head>
<body>
<div class="container">
<div th:if="${validationError}" th:each="error : ${validationError}">
<label class="text-danger" th:text="${error}"></label>
</div>
<h1>ユーザー編集</h1>
<form th:action="@{/user/update}" th:object="${userUpdateRequest}"
th:method="post">
<input type="hidden" th:field="*{userid}" />
<div>
<div class="row mx-md-n5">
<div class="col-2 pt-3 border bg-light">
名前<span class="text-danger">※</span>
</div>
<div class="col py-2 border">
<input type="text" class="form-control" th:field="*{name}">
</div>
</div>
<div class="row mx-md-n5">
<div class="col-2 pt-3 border bg-light">
メールアドレス<span class="text-danger">※</span>
</div>
<div class="col py-2 border">
<input type="text" class="form-control" th:field="*{email}">
</div>
</div>
</div>
<br />
<div class="text-center">
<input type="submit" class="btn btn-primary" value="確定">
<a href="/" class="btn btn-secondary">戻る</a>
</div>
</form>
</div>
</body>
</html>
user/userlist.html
ユーザー一覧画面用です。SpringBootが実行されたときに初期画面として表示される画面です。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="common/head :: head_fragment(title = 'ユーザー一覧', scripts = ~{::script}, links = ~{::link})"></head>
<body>
<div class="container">
<h1>ユーザー一覧</h1>
<div class="float-end">
<a th:href="@{/user/add}" class="btn btn-primary">新規登録</a>
</div>
<br>
<div th:if="${userlist}">
<table class="table table-striped">
<thead>
<tr>
<th>ユーザーID</th>
<th>名前</th>
<th>メールアドレス</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${userlist}" th:object="${user}" class="align-middle">
<td th:text="*{userid}"></td>
<td th:text="*{name}"></td>
<td th:text="*{email}"></td>
<td align="right">
<a th:href="@{/user/{userid}/edit(userid=*{userid})}" class="btn btn-primary">編集</a>
<a th:href="@{/user/{userid}/delete(userid=*{userid})}" class="btn btn-secondary">削除</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
アプリケーションを起動
上記実装が終わったらSpringBootを起動します。以前紹介した記事を元に構築を行っていれば
Spring Boot Dashboadが入っているはずなので起動させます。
ターミナルでエラーがなければ以下をクリックします。
VSCode上に以下のようにWEB画面が立ち上がればOKです。今回は事前にSQL Serverに4件のテストユーザーのデータを入力しています。
単純にユーザーテーブルの情報を一覧表示・新規登録・編集・削除ができるようになっています。
長々と書きましたが以上で終了です。
まとめ
いろいろなサイトなどを参考にしながら作らせてもらいましたが、SpringBoot2でJavaのバージョンは11のものが多かったのでちょっと苦戦しました。特にjakarta移行問題で・・・・
これで簡単なSpringBootのアプリケーションが作れたので次は単体テストコードを書いていこうと思います。最終的にはGitHubActionsで自動ビルド&テストができればいいなと思います。
参考にしたサイト
当プログラムを作成する際に以下のサイトがわかりやすく参考にさせていただきました。
MyBatis + SpringでWebアプリ(CRUD)を作成するはじめに MyBatisとSpring Bootを利用して「検索画面」「登録画面」「編集画面」の作成方法を紹介します。 本記事で作成する画面は次のとおりです。 ■検索画面 ■新規登録画面 ■編集画面
コメント