EurekaMoments

ロボットや自動車の自律移動に関する知識や技術、プログラミング、ソフトウェア開発について勉強したことをメモするブログ

JavaとSpring BootによるWebアプリ開発の基礎

目的

今後も長くソフトウェアエンジニアとしてのキャリアを築いていくことを
考えたときに、Webやネットワーク、データベースといった技術を利用した
システムを開発できる能力は必須だなと思うようになりました。

そこで、ちょうど今年から仕事でJavaを扱うようになったのと、つい最近
上記の書籍が発売になったのをきっかけに、Webアプリ開発を勉強し始めました。

今回は、その勉強の一環であるタスク管理アプリと、その開発を通じて学んだ
Webアプリ開発の基礎についてまとめたいと思います。

目次

Webアプリとは

Webアプリケーションとは、ネットワーク通信によりインターネット上に
あるWebサーバーと連携して動作するアプリケーションです。その本体は、
Webサーバー上で動作しており、それをユーザーはWebブラウザを通して
Webアプリケーションを操作します。

Webアプリの仕組み

Webアプリ内部の構成は、下の図のようになっています。
大きく分けて2つのパートに分かれており、一つはユーザーが操作する
ブラウザ側をクライアントサイド、もう一つはプログラム本体が動作
しているサーバーサイドです。

www.kagoya.jp

HTTP通信

Webブラウザとサーバーは、HTTP(HyperText Transfer Protocol)という
プロトコルで通信を行います。HTTPとは、Web上で文書のテキスト、
レイアウト定義、画像、動画、スクリプトなどを取り込んだHTML文書
などのリソースを取り出すことを可能にするプロトコルです。

developer.mozilla.org

ブラウザなどのクライアント側から送られるデータをHTTPリクエスト、
それに対してサーバーが回答として返すデータをHTTPレスポンスと
いいます。

まず、Webブラウザからのリクエストをサーバー側のプログラムが受け取り、
それに応じたデータの格納や取り出し、演算などの処理を行います。
そして、それらの結果を表示するためのコンテンツを生成し、レスポンス
としてブラウザに返すという一連の処理が行われます。
また、HTTPによるリクエストには、こちらの記事で解説されているように
さまざまな種類があり、目的に応じて使い分けられます。

qiita.com

HTTP通信とHTTPS通信

HTTP通信は保護がされていないため、ブラウザとサーバー間のデータの
やり取りが丸見えになってしまうというデメリットがあります。これは、
そのデータが暗号化されていない状態であるということです。

それに対して、やり取りするデータを盗み見られても分からないように
暗号化した通信方式として、HTTPS(HyperText Transfer Protocol Secure)が
あります。
HTTPS通信によってデータが守られている仕組みについては、
こちらの記事で詳しく説明されているので参照ください。

securitynews.so-net.ne.jp

フレームワークを利用したアプリ開発

大規模なアプリを開発する際、全てのプログラムを自分達で作ろうと
すると、大量のプログラムを自分で書かなければならず、大変な労力を
要します。こういった問題を解決してくれるのがフレームワークです。

フレームワークとは、アプリを開発する際に必要となる汎用的な機能や
枠組みをライブラリとして提供するソフトウェアです。また、単なる
ライブラリだけでなく、再利用可能なクラスやAPI、アプリの雛形として
利用できるテンプレート、標準的な設定ファイルの集合体であり、
これらを利用することで、作業効率を上げて開発期間を大幅に短縮できる
ようになります。

Spring BootによるWebアプリ開発

JavaによるWebアプリ開発向けフレームワークのデファクトスタンダード
としてSpring Frameworkがあります。小規模なWebサイトから大規模な
システムまで幅広くサポートする様々なフレームワークの集合体であり、
それらの中から複数のフレームワークを組み合わせて使用することになり
ます。

camp.trainocate.co.jp

一方で、様々な機能が提供されている反面、設定が複雑で環境構築に手間が
掛かったり、機能を適切に選んで使い分けるのが難しいという問題があります。
それを解決するために開発されたのがSpring Bootです。

spring.io

アプリを動かすのに必要最低限なクラスが用意され、指定されたメソッドを
呼び出すだけで起動できるような仕組みになっています。また、アプリの
実行基盤となるWebサーバーを簡単に埋め込めるのも大きな特徴です。

Spring Bootを用いたタスク管理アプリの開発

今回の勉強に活用して「プロになるJava」では、第21章と22章にて
Spring Bootでタスク管理Webアプリを作るというコーナーがあり、
こういう技術書におけるワンコーナーとしてはとても本格的な内容に
なっています。

ここからは、このタスク管理アプリ開発の内容と、それを通じて学んだ
ことをまとめていきます。

要件定義

今回開発するタスク管理アプリは、Webブラウザ上でユーザーが下記の
要件を満たせるものとします。

  • タスクをデータベースに登録できる。
  • 登録したタスクを参照して確認できる。
  • 登録したタスク情報を編集できる。
  • 登録したタスク情報を削除できる。

そして、これらをユースケース図で表すとこのようになります。

また、タスク情報として登録したい項目は下記の通りとします。

  • タスクリストのID
  • タスク名
  • 終了期限
  • 終了状態

これらをデータベースにタスクリストというテーブルとして登録
します。それをクラス図で表すとこのようになります。

開発用プロジェクトの作成

ここからは、開発環境構築としてJavaとIntelliJ IDEAのインストールが
完了していることを前提に話を進めていきます。インストール方法に
ついてはこちらの記事を参照ください。

www.eureka-moments-blog.com

Spring Bootを使ったアプリ開発のプロジェクトを作る方法として、
Spring InitializerというWebサービスを使うことが推奨されています。

https://start.spring.io/

Springプロジェクトが公式に提供している無料のサービスであり、
自分で一からプロジェクトを作るよりも簡単にアプリの雛形を
作成することができます。

今回は、各種設定項目をこちらのように設定します。

そして、一通りの設定を終えた後にGenerateボタンを押すと、
プロジェクトの雛形がzip形式でダウンロードされるので、それを
任意の場所に解凍すれば完了です。

プロジェクトの中身の確認

先程、解凍したプロジェクトをIntelliJ IDEAで開きます。すると、
こちらのようにソースコードやテストコードのディレクトリ、
外部ライブラリなど、必要な構成が一通り揃った状態に
なっているはずです。

また、余計なファイルやディレクトリはGitのコミット対象と
しないように、あらかじめ.gitignoreが書いてあるのも嬉しいですね。

このとき、ソースコードはsrc/main/javaのディレクトリ以下に置かれ、
初期状態では、JavaTaskListApplicationというSpring Initializerによって
自動生成されたクラスだけがあります。そしてこれが、今回開発する
タスク管理アプリのmainメソッドを持ちます。

JavaTaskListApplicationのソースコードはこちらのようになっており、
何かしら特別な理由がない限りは、この自動生成された状態のまま
使うことができます。

package jp.shisato.javaproject.javatasklist;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JavaTaskListApplication {
    public static void main(String[] args) {
        SpringApplication.run(JavaTaskListApplication.class, args);
    }
}

続いて、プロジェクトのルートディレクトリ直下に配置されている
pom.xmlというファイルを開いてみましょう。pom.xmlとは、
JavaプログラムのビルドシステムであるMavenでプロジェクトを
ビルドする際に必要な、依存するライブラリの情報やプラグインの
設定などをまとめたファイルです。

www.techscore.com

このとき、pom.xmlに記述される各要素には次のようなものがあります。

特に重要なのが、依存するライブラリについての設定を記述する
dependenciesの部分です。

プロジェクトを作成した直後の状態では、Webアプリを作るために
必要な各種APIを使うためのspring-boot-starter-web、Spring Bootで
作ったアプリをユニットテストするためのspring-boot-starter-testが
追加されています。

ここから先は、必要に応じてライブラリの依存関係を同じように追加
していくことになります。

プラグインの設定

Spring Bootは、作成したアプリから実行可能なJARファイルを生成する
ためのMavenプラグインを提供しています。

spring.pleiades.io

qiita.com

プロジェクト作成時に、pom.xmlにもこのように自動で記述されるのですが、
そのままだとプラグインが見つからないとエラーを吐くことがあります。

このようにSpring Bootが提供する標準の設定をビルド環境に組み込むには、
spring-boot-starter-parentを親プロジェクトとして指定している必要が
あります。

そのため、子プロジェクトであるspring-boot-maven-pluginの方では、
親プロジェクトのバージョンを記述しておく必要があり、それを
こちらのように追記することで、今回の問題は解決されます。

IntelliJからアプリをビルド・実行するための設定

まず、このプロジェクトで使用するJDKを設定します。
IntelliJ画面の左上から「ファイル」->「プロジェクト構造」と選択すると
こちらのような画面が開きます。

ここで、左側メニューから「プロジェクト」と選び、右側のSDKにて
既にインストールしているJDKを選択しましょう。そのあとに「OK」を
押して、JDKの設定を完了します。

次に、IntelliJから直接アプリをビルド、実行できるようにするための
設定をします。これをやっておくと、コードや設定ファイルの修正を
するたびにコマンドラインからビルドや実行のコマンドを打つ必要が
無くなるので非常に便利です。

IntelliJ画面右上にあるこちらのGUIから下矢印を押し、プルダウンで
表示されるメニューから「実行構成の編集...」を選択します。

すると、 「実行/デバッグ構成」という画面になるので、その左上に
にある「+」アイコンを押し、表示される実行構成タイプのリストから
「アプリケーション」を選択します。

そして、各項目の設定を次のようにします。

ここでは、任意の名前を入力し、Javaの実行環境には「Java 17」を
指定します。そして、その右隣にあるメインクラスには、アプリを
実行する際に最初に呼び出されるmainメソッドを持つクラスを
指定します。

右端の赤丸で囲まれたアイコンを選択すると、このように指定する
クラスを名前で検索、あるいはファイルツリーから検索できるように
なるので、そこからmainメソッドを持つJavaTaskListApplicationを
選択して「OK」を押せば完了です。

アプリのビルドと実行

ここまでの設定ができたところで、アプリをちゃんとビルド、実行
できるか確認しておきましょう。IntelliJのこちらで実行したいアプリが
選択された状態で、右側にある再生アイコンをクリックすれば、
アプリがビルド、実行されます。

特に問題が無ければ、IntelliJの実行ウィンドウにてこのような実行ログが
表示されるはずです。

この実行ログを見ると、こちらのように「Tomcat」という名前が
ところどころで表示されていることが分かります。

今回のようにSpring Initializerで作成されたプロジェクトには、
Spring Webというライブラリが依存関係に追加されます。そして、
Spring WebにはTomcatという組み込みWebサーバーが付属しており、
アプリ実行時にはまずサーバーのTomcatが起動し、その上で
アプリが実行される仕組みになっています。

Tomcatとはサーブレットコンテナとも呼ばれ、Webサーバー上で
動くJavaプログラム(これをサーブレットという)を動かすための
ソフトです。これにより、自前でWebサーバーを用意しなくても
自身のPC上でサーブレットを動かせるようになります。
https://wa3.i-3-i.info/word12843.html

Javaサーブレットについてはこちらの記事で詳しく解説されている
ので参照ください。
www.fenet.jp

同じく実行ログを見ると、「Tomcat started on port(s): 8080 (http)」と
も表示されているのが分かりますが、これはTomcatが8080番ポートで
起動していることを意味しています。
デフォルトでは、Tomcatはlocalhost(現在作業しているPC)上で起動
するようになっており、Webブラウザから「http://localhost:8080/
にアクセスすると、今回開発するタスク管理アプリにアクセスする
ことができます。

アプリをコマンドラインから起動できるようにする方法

IntelliJから直接アプリをビルド、起動できるのは便利ですが、
実際のサーバー上で実行するにはコマンドラインから起動
できる必要があります。
そのための手段としては、アプリの各種ファイルを圧縮して
実行可能な形式にしたJAR(Java Archive)ファイルまとめるという
やり方があります。

まずは、JARファイルを作成するためのビルドを実行します。
IntelliJのGUIの右端にあるMavenメニューから「アプリ」->
「ライフサイクル」->「package」と進み、再生ボタンを押し
「Mavenビルドの実行」を行います。

するとビルドが実行され、実行ウィンドウでこうのように表示
されたら完了です。

すると、プロジェクトのtargetディレクトリに、pom.xml内に
記述されたartifactId-version.jarという名前のJARファイルが
生成されます。

生成されたJARファイルには、アプリ実行に必要なWebサーバー
(Tomcat)も含まれているので、あとはコマンドラインから
こちらのjavaコマンドによって実行することができます。

$ java -jar hoge.jar

JARファイルについてはこちらの記事で詳しく解説
されているので参照ください。

www.tech-teacher.jp

Maven Wrapperとは

Spring Initializerで作成したプロジェクトのディレクトリを見ると、
こちらのような2つのファイルが置かれていると思います。

これらはMaven Wrapperといって、そのプロジェクトに必要な
バージョンのMavenをインストールし、ローカル環境に素早く
Mavenを用意するためのツールです。
JavaのビルドシステムとしてMavenを使う場合、依存する
ライブラリのバージョン管理などをpom.xmlで管理しますが、
各開発者が使うMavenのバージョンまでは指定できません。
そこで、プロジェクトで使うMavenのバージョンを固定する
ために使えるのがMaven Wrapperという訳です。

これら2つのファイルの違いは、

  • mvnw: Maven Wrapper経由でmvnコマンドを実行するためのファイル
  • mvnw.cmd: mvnwのWindows版。Windowsで使う場合はこっちを使う。

となっています。例えば、こちらのようにコマンドを打って実行
すれば、プロジェクトのクリーンとコンパイル、JARファイルへの
パッケージングといった一連のビルドを実行してくれます。

./mvnw clean package

Maven Wrapperについては、こちらの記事にさらに詳しい解説が
あるので参照ください。

area-b.com

zephiransas.github.io

qiita.com

MVCモデル

Webアプリは一般的に、「モデル」「ビュー」「コントローラ」という
3つのパーツに分けて考えられ、それぞれの頭文字をとってMVCモデルと
呼ばれます。そして、これら3つのパーツはそれぞれ下記のような役割を
担っています。

  • モデル: アプリ内で使用するデータを管理、処理するロジック
  • ビュー: 表示したり、入力する機能の処理をするUI
  • コントローラ: ユーザーのリクエストに応じたモデルとビューの制御

このように役割に応じてプログラムを分割し、それぞれの独立性を
高めておくことで、開発が効率化しプログラムの再利用性もしやすく
なるというメリットがあります。

例えばモデルの場合だと、データの管理をモデルに集めることで、
ビューやコントローラからデータ管理の実装を取り除き、何か
問題が起きたときは、モデルの実装を疑えばいいことになります。

より詳細な説明はこちらの記事に書かれているので参照ください。 tsuyopon.xyz
www.geekly.co.jp

コントローラを実装するためのアノテーション

アノテーションとは、Javaプログラムにおいてコード部分だけでは
命令しきれない情報を伝えるための注釈です。そして、Spring Bootに
よってコントローラを実装する際には、下記の2つがよく使われます。

  • @RestController
  • @Controller

これらをクラス定義に付けることで、Spring Bootはそのクラスが
コントローラだと判断し、HTTPに関する処理を行うようになります。

@Controllerは、戻り値としてビュー(HTML)を返す際に使うアノテーション
であり、主にWebページ用のコントローラで使用します。
そして、@RestControllerは、JSONやXMLを返すAPIサーバー用として
使用します。このときのメソッドの戻り値はビューではなく、
レスポンスのコンテンツになります。
こちらの記事でさらに詳しく解説されているので参照ください。

qiita.com

データベースの導入

今回開発するタスク管理アプリでは、登録したタスク情報を
保持したり、編集したりできる必要があります。そのため、
アプリの外部にそういったデータを保存しておく手段として
データベースを利用します。
it-trend.jp

こういったコンピュータシステム上でデータベースを管理する
ソフトウェアをDBMS(Database Management System)と呼びます。
これには、内部構造や管理の仕方により下記のような種類に
分類されます。

  • リレーショナルデータベース
  • ドキュメントデータベース
  • グラフデータベース
  • オブジェクト指向データベース

それぞれにメリット/デメリットがあり、目的や動作環境などに応じて
適したものを選ぶことになります。なかでも広く使われているのは
リレーショナルデータベース(RDBMS)であり、データをレコード(行)と
カラム(列)からなるテーブル(表)の形で格納するものです。

www.tableau.com

SQLによるデータベースの操作

リレーショナルデータベースを操作する手段としてSQL
(Structured Query Language)があります。決められた文法に
従って命令を記述し実行することで、データの追加や変更、
削除、検索などの処理が可能となります。

products.sint.co.jp

そのなかでも、よく使う操作としては下記のものがあります。

  • CREATE
  • INSERT
  • SELECT
  • UPDATE
  • DELETE

まずCREATEは、テーブルを作成するためのSQL文です。
例えば、exampleという名前のテーブルを作る場合は、
このように書きます。

CREATE TABLE example (
    id VARCHAR(3) PRIMARY KEY,
    message VARCHAR(256)
);

()の中で宣言しているのは、カラムの名前とデータの型です。
この例では、idとmessageという2つのカラムを宣言し、それぞれの
データ型はVARCHARという可変長文字列です。このとき、カッコ内
の数値は文字列の最大サイズを表します。そして、PRIMARY KEYとは
主キーといい、データを特定するために使う、値が省略できず重複を
許さないカラムであることを表します。

続いてINSERT文は、テーブルにデータを格納するためのSQL文です。
例えば、先程作成したexampleというテーブルに、idが'001'、messageが
'Good morning'というレコードを追加したい場合は、このように
書きます。

INSERT INTO example
    VALUES ('001', 'Good morning');

また、このように複数のレコードをまとめて追加することもできます。

INSERT INTO example
    VALUES
        ('002', 'Good afternoon'),
        ('003', 'Good evening'),
        ('004', 'Good night');

続いてSELECT文は、テーブルの中身を確認するためのSQL文です。
例えば、exampleテーブルからmessage列のレコードを参照して
中身を表示させる場合は、このように書きます。

SELECT message
    FROM example;

また、このように複数の列を指定して表示することもできます。

SELECT id, message
    FROM example;

全てのレコードを一覧表示させたいときはこのように書けます。

SELECT *
    FROM example;

続いてUPDATE文は、既に登録されているレコードを更新する
ためのSQL文です。例えば、exampleテーブルにあるidが'001'の
レコードの内容を更新する場合はこのように書きます。

UPDATE example
    SET message = 'Hello'
    WHERE id = '001';

最後は、テーブルに登録されているレコードを削除する
DELETEです。こちらのように書くことで、指定した
レコードをテーブルから削除することができます。

DELETE FROM example
    WHERE id = '001';

Javaプログラムからのデータベース接続

JavaプログラムからSQLでデータベースを操作するためには、
まずJavaプログラムからデータベースに接続する必要があります。
その際には、java.sqlというパッケージで基本機能として提供
されているJDBC(Java DataBase Connictivity)というAPIを
利用します。
そして、今回のようにSpring Frameworkを使用してアプリを
開発する場合は、Spring JDBCという専用ライブラリが提供されて
おり、それを使うことになります。
atmarkit.itmedia.co.jp

H2データベースの利用

Javaアプリで利用できる数あるDBMSの中から、今回はこちらの
H2 Database EngineというRDBMSを利用します。
www.h2database.com これはプログラム本体のサイズが小さいため、軽量に動作する
という特徴があります。また、単一のJARファイルで構成されて
おり、OSへのインストール無しで使えるので、Webアプリにも
組み込みやすいとされています。
it-jog.com

使用するには、まずpom.xmlに依存関係を追加します。
ここでは、H2を組み込むための依存関係、そしてSpring JDBC
を使うための2種類の依存関係を以下のように追加します。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

続いて、プロジェクトディレクトリのsrc/main/resources以下にある
application.propertiesというファイルに、JDBCドライバやデータベース
へのアクセスに関する設定を記述します。これは、Spring Bootアプリ
で使用する環境変数などの設定を記述するためのファイルであり、
こちらのように「設定項目名=設定値」という形式で記述します。

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/taskdb
spring.datasource.username=gihyo
spring.datasource.password=gihyodb

spring.datasource.initialization-mode=always

spring.h2.console.enabled=true

これらの項目は、上から順に下記のようになっています。

  • JDBCドライバのクラス名
  • データベースのパス
  • データベース接続のユーザー名
  • データベース接続のパスワード
  • SQLファイルを利用したデータベース初期化を行うか
  • H2コンソールを有効にするか否か

中でも重要なのは2番目のデータベースへのパスです。
H2には、データベース本体を単一ファイルに保存するか、メモリ上
のみに記録するか、の2種類のモードがあり、今回は前者を選択
します。
H2のための設定として、ここでは「jdbc:h2:」+ ファイルのパスという
形式で書く必要があり、上記のようにjdbc:hs:~/taskdbと書くと、
自分のPCのホームディレクトリにtaskdbという名前で、データベース
本体のファイルがアプリ起動時に自動生成されます。

データベーステーブルの初期化

先述したように、今回はJavaアプリとデータベースを接続する方法
として、Spring Frameworkに用意されている「Spring JDBC」という
ライブラリを使用します。

Spring JDBCを使うと、application.propertiesで設定する
spring.datasource.initialization-modeをalwaysにすることで、
アプリ起動時に事前に用意したSQL文を自動実行できるように
なります。そして今回はこの機能を使って、タスク情報を格納
するためのテーブルを、アプリ起動時に作成するようにします。

実行させるSQL文は、~.sqlという名前のファイルに記述し、
それをsrc/main/resourcesディレクトリに配置します。
実行させるSQL文はこのようにします。

CREATE TABLE IF NOT EXISTS tasklist (
    id VARCHAR(8) PRIMARY KEY,
    task VARCHAR(256),
    deadline VARCHAR(10),
    done BOOLEAN
);

ここで使用しているIF NOT EXISTSという命令文は、同じ名前の
テーブルが存在しなければ新規作成するというものです。そのため、
アプリの初回起動時のみ実行されることになります。

データベース操作用クラスの作成

ここからはコントローラの実装に入りますが、まずはデータベースに
アクセスしてテーブルを操作する役割を担う部分を作ります。
コントローラ本体と、データベース操作を別々のクラスに分割する
ことで、役割分担が明確になり、全体の見通しが良くなるという
メリットがあります。
また、このようにデータベースにアクセスするための窓口となる
オブジェクトのことをDAO(Data Access Object)と呼びます。

今回は、各タスク情報をリストアップし、それらをデータベースに
格納したり更新したりすることから、TaskListDaoという名前の
クラスを作りました。そのソースコードがこちらです。

package jp.shisato.javaproject.javatasklist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class TaskListDao {
    private final JdbcTemplate jdbcTemplate;
    record TaskItem(String id, String task, String deadline, boolean done) {}

    @Autowired
    TaskListDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void add(TaskItem taskItem) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(taskItem);

        SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName("tasklist");

        insert.execute(param);
    }

    public List<TaskItem> findAll() {
        String query = "SELECT * FROM tasklist";

        List<Map<String, Object>> result = jdbcTemplate.queryForList(query);
        List<TaskItem> taskItems = result.stream()
                .map((Map<String, Object> row) -> new TaskItem(
                        row.get("id").toString(),
                        row.get("task").toString(),
                        row.get("deadline").toString(),
                        (Boolean)row.get("done")
                )).toList();

        return taskItems;
    }

    public String getId(int index) {
        List<TaskItem> taskItems = findAll();

        if (taskItems.size() <= 0) { return "-1"; }

        return taskItems.get(index).id().toString();
    }

    public int delete(String id) {
        int number = jdbcTemplate.update("DELETE FROM tasklist WHERE id = ?", id);

        return number;
    }

    public int update(TaskItem taskItem) {
        int number = jdbcTemplate.update(
                "UPDATE tasklist SET task = ?, deadline = ?, done = ? WHERE id = ?",
                taskItem.task(),
                taskItem.deadline(),
                taskItem.done(),
                taskItem.id()
        );

        return number;
    }
}

JdbcTemplateクラス

作成したTaskListDaoクラスは、JdbcTemplateクラスのオブジェクトを
メンバーとして持ちます。これはデータベースへのアクセスを簡単に
してくれるクラスであり、接続先や接続方法などの情報を保持する
必要があります。

private final JdbcTemplate jdbcTemplate;

コンストラクタではこれを初期化する必要がありますが、
コンストラクタに@Autowiredというアノテーションを付ける
ことで、Spring Bootでは適切な初期設定を自動で行ってくれる
ようになっています。

@Autowired
TaskListDao(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
}

DI(Dependency Injection: 依存性の注入)

上記のコードを見たら分かるように、TaskListDaoクラスは
JdbcTemplateクラスに依存したものになっており、その依存性に
関する情報をSpringではapplication.propertiesのような
外部ファイルに記述し読み込ませるような仕組みになっています。
そのような仕組みをDI(Dependency Injection)といい、自前で
各種初期設定をいちいち定義しなくてよくなるというメリットが
あります。

qiita.com

タスク情報を保持するモデルの作成

TaskListDaoクラスはタスク情報をデータベースに格納したりするのが
役目なので、そのタスク一つ当たりの情報を格納する部分を実装して
おきます。
これは、MVCモデルにおける「モデル」に該当する部分であり、
下記の情報を保持するものとします。

  • id(String): タスクのID
  • task(String): タスク名
  • deadline(String): タスクの期限
  • done(boolean): 完了したか否か

そして、これらをこちらのようなレコードとして宣言します。

record TaskItem(String id, String task, String deadline, boolean done) {}

データベースにタスク情報を追加するメソッドの実装

タスク情報を格納する部分ができたら、今度はそれをデータベースの
テーブルに追加するメソッドを作ります。実際のコードは下記の
ようになります。

public void add(TaskItem taskItem) {
    SqlParameterSource param = new BeanPropertySqlParameterSource(taskItem);

    SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName("tasklist");

    insert.execute(param);
}

ここでポイントになるのは、SimpleJdbcInsertというクラスです。
これはテーブルへデータを追加するためにSpring JDBCに用意
されているクラスであり、これが持つexecuteメソッドをを使えば、
SQLのINSERTを発行しなくてもデータの追加ができます。

SimpleJdbcInsertを使うにはまず、SqlParameterSourceというクラスの
オブジェクトを用意します。これはBeanPropertySqlParameterSource
の実装クラスであり、そのコンストラクタに追加したいデータである
TaskItemのオブジェクトを渡します。

次に、SimpleJdbcInsertのオブジェクトを用意します。
コンストラクタに、対象データベースに紐づいている
JdbcTemplateのオブジェクトを渡します。
そして、そのオブジェクトが持つwithTableNameメソッドを
呼び出し、データを追加するテーブルの名前を指定します。

こういったやり方により、追加データを持つTaskItemオブジェクトを
そのまま渡せばいいだけになる、というメリットが生まれます。

タスク情報を全て取得するメソッドの実装

次は、テーブルに格納されているタスク情報を一通り取得して、
リスト形式で返すメソッドを実装します。
実装したこちらのコードでは、タスク情報を取得するための
SELECT文を組み立てて実行し、出力するTaskItemのListオブジェクト
を作成しています。

public List<TaskItem> findAll() {
    String query = "SELECT * FROM tasklist";

    List<Map<String, Object>> result = jdbcTemplate.queryForList(query);
    List<TaskItem> taskItems = result.stream()
            .map((Map<String, Object> row) -> new TaskItem(
                    row.get("id").toString(),
                    row.get("task").toString(),
                    row.get("deadline").toString(),
                    (Boolean)row.get("done")
            )).toList();

    return taskItems;
}

SELECT文を組み立てる方法の一つとして、ここでは
queryForListメソッドを使っています。これにSQL文を
Stringとして引数に渡すと、その検索結果をListで
返してくれます。

このときの戻り値は、列名をキー、その中身のデータを
値とするMapオブジェクトのListになります。そして、
queryForListメソッドの戻り値から要素を1つずつ取り出し、
それぞれをTaskItemオブジェクトに変換してListに入れなおす
という処理を行っています。

データベースからタスク情報を削除するメソッドの実装

今度は、タスク情報の一つであるidを指定することでそれに
該当するレコードをテーブルから削除するメソッドを実装します。
こちらが実装したコードです。

public int delete(String id) {
    int number = jdbcTemplate.update("DELETE FROM tasklist WHERE id = ?", id);

    return number;
}

jdbcTemplateオブジェクトが持つupdateメソッドを呼び出し、
その第1引数にSQLのDELETEを実行する文字列を渡します。
文字列中の?は列の値であり、第2引数にはそれに実際に当てはまる
値を渡します。

データベースのタスク情報を更新するメソッドの実装

今度は、既にテーブルに登録されているタスク情報を更新する
メソッドを実装します。こちらが実装したコードです。

public int update(TaskItem taskItem) {
    int number = jdbcTemplate.update(
            "UPDATE tasklist SET task = ?, deadline = ?, done = ? WHERE id = ?",
            taskItem.task(),
            taskItem.deadline(),
            taskItem.done(),
            taskItem.id()
    );

    return number;
}

タスク情報オブジェクトを入力引数として受け取り、
それに基づいてSQLのUPDATE文を作成します。
UPDATE文の文字列をjdbcTemplateオブジェクトが持つ
updateメソッドの第1引数に渡し、テーブルに登録する
実際の値を第2引数以降に渡します。
そして、updateメソッドが返すのは、それにより更新された
レコードの行数となっています。

データベースから特定のタスクidを取得するメソッドの実装

最後に、後述するユニットテスト用に特定のタスクidをテーブルから
取得するメソッドを実装します。こちらが実装したコードです。

public String getId(int index) {
    List<TaskItem> taskItems = findAll();

    if (taskItems.size() <= 0) { return "-1"; }

    return taskItems.get(index).id().toString();
}

まずは、既に実装したfindAllメソッドを使い、テーブルに
登録されているタスク情報のリストを取得します。そして、
そのリストが空じゃなければインデックスで指定したところに
あるタスク情報を参照し、それに含まれるidの文字列を返します。

コントローラクラスの作成

ここまでの工程で、データベースを直接操作するDAOクラスの
実装はできたので、次はいよいよ「モデル」と「ビュー」を
制御する「コントローラ」の実装をしていきます。
最終的にはこのようなコードになります。

package jp.shisato.javaproject.javatasklist;

import jp.shisato.javaproject.javatasklist.TaskListDao.TaskItem;

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 java.util.List;
import java.util.UUID;

@Controller
public class HomeController {
    private final TaskListDao dao;

    @Autowired
    HomeController(TaskListDao dao) {
        this.dao = dao;
    }

    @GetMapping("/list")
    String listItems(Model model) {
        List<TaskItem> taskItems = dao.findAll();

        model.addAttribute("taskList", taskItems);

        return "home";
    }

    @GetMapping("/add")
    String addItem(@RequestParam("task") String task,
                   @RequestParam("deadline") String deadline) {
        String id = UUID.randomUUID().toString().substring(0, 8);

        TaskItem item = new TaskItem(id, task, deadline, false);

        dao.add(item);

        return "redirect:/list";
    }

    @GetMapping("/delete")
    String deleteItem(@RequestParam("id") String id) {
        dao.delete(id);

        return "redirect:/list";
    }

    @GetMapping("/update")
    String updateItem(@RequestParam("id") String id,
                      @RequestParam("task") String task,
                      @RequestParam("deadline") String deadline,
                      @RequestParam("done") boolean done) {
        TaskItem taskItem = new TaskItem(id, task, deadline, done);

        dao.update(taskItem);

        return "redirect:/list";
    }
}

@Controllerアノテーションによるコントローラの作成

先述したように、@Controllerアノテーションは戻り値として
ビュー(HTML)を返すためのアノテーションです。ここで作成する
コントローラは、データベースにあるタスク情報をWebページに
表示したりするのが役目なので、それを形成するHTML文書を
返すということで、クラス定義の際に@Controllerアノテーション
を付けるようにします。

@Controller
public class HomeController {

}

TaskListDaoクラスのフィールドへの追加

先に作成したTaskListDaoクラスは、データベースと直接
連携するのが役目であるクラスです。それをフィールドに
追加することによって、HomeControllerクラスが
データベースと連携できるようにします。

@Controller
public class HomeController {
    private final TaskListDao dao;

    @Autowired
    HomeController(TaskListDao dao) {
        this.dao = dao;
    }
}

TaskListDaoクラスのときと同様に、コンストラクタには
@Autowiredアノテーションを付けるようにします。
これによって、TaskListDaoクラスに適切に初期化された
JdbcTemplateオブジェクトが渡されるように、HomeController
クラスのコンストラクタに適切に初期化されたTaskListDao
オブジェクトが渡されるようになります。

タスクを追加するエンドポイントの作成

データベースに追加したいタスク情報をブラウザから
HTTPリクエストとして受け取るためのメソッドを作ります。
このように、ブラウザつまりクライアント側との窓口と
なるメソッドをエンドポイントと呼びます。

@GetMapping("/add")
String addItem(@RequestParam("task") String task,
               @RequestParam("deadline") String deadline) {
    String id = UUID.randomUUID().toString().substring(0, 8);

    TaskItem item = new TaskItem(id, task, deadline, false);

    dao.add(item);

    return "redirect:/list";
}

ここでは@GetMappingというアノテーションを
メソッドに付けます。こうすることで、この
メソッドはクライアントからのHTTPリクエストを
処理するものだとSpring Bootが判断し、そのための
適切な処理を自動で行ってくれるようになります。

また、このアノテーションには"/add"という文字列を
与えています。これはvalue属性という値であり、
URLのどのパスに対するリクエストがこのメソッドに
処理されるのかを指定することができます。

例えば、このアプリの公開URLが「http://www.hoge.co.jp」なら、
リクエストが「http://www.hoge.co.jp/add」に対して送られた
場合に、上記のメソッドが実行されます。

そして、このメソッドには"task"と"deadline"という
2つの引数が渡されますが、これらには@RequestParamと
いうアノテーションを付けています。
こうすることで、これら2つの引数がHTTPリクエストの
パラメータと関連付けられるようになります。

HTTPリクエストにおけるGETとPOST

HTTPリクエストにはいくつかの仕様が定義されており、その中でも
代表的なのがGETとPOSTです。

これらは表面上は一見同じ動作をしているように見えますが、
実装したい機能の目的に応じてより適切な方を選ばないと
いけません。

上記のaddItemメソッドには@GetMappingアノテーションを
付けていますが、これはGETメソッド専用のものです。
それとは別に@RequestMappingというものもありますが、
これはGETとPOSTの両方に対応しています。

GETとPOSTの詳細な違いについてはこちらで詳しく解説されています。

qiita.com

テンプレートエンジンによるビューの作成

コントローラはリクエストを受け取った際、それに
対応したWebページを生成するためのHTML文書を
返すことでページの表示を制御します。

この時の一番シンプルな方法は、HTML文書の
文字列をStringオブジェクトで生成し、それを
戻り値として返すことです。

ただしそれでは、HTMLコードをJavaコードに直接
書くことになるので、ページのちょっとした変更だけ
でもJavaコードを変更しないといけません。
そのため最近では、テンプレートエンジンという
ツールにより、ページのデザインを定義するHTMLを
テンプレート化して別ファイルとして作成しておく
ことで、Javaで作るコントローラ部分とHTMLによる
ビュー部分を分けるのが主流となっています。

Thymeleafの利用

Spring Frameworkと連携して使えるテンプレート
エンジンには様々な種類がありますが、今回は
Spring Bootと相性がいいとされているThymeleaf
を利用します。

www.nttdata.com

style.potepan.com

まずは、pom.xmlに依存関係の設定を下記のように
追加し、そのあとでMavenモジュールの再ロードを
行います。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.6.6</version>
</dependency>

HTMLテンプレートの作成

次に、Webページにおけるタスクの追加機能、一覧表示機能、
削除機能、更新機能のビューを構成するHTMLテンプレートを
作成します。ここでは、home.htmlというテンプレートファイルを
作成し、src/resources/templatesディレクトリ以下に置きます。

そして、そのテンプレートファイルの中はこのように記述します。

<!DOCTYPE html>
<!--Declaration as Thymeleaf template-->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Task List Manager</title>
    <link th:href="@{/home.css}" rel="stylesheet">
</head>

<body>
    <h1>Task List Manager</h1>

    <div class="task_form">
        <h2>Register task</h2>

        <form action="/add">
            <label>Task</label>
            <input name="task" type="text" />
            <label>Deadline</label>
            <input name="deadline" type="date" />
            <input type="submit" value="Register" />
        </form>
    </div>

    <div class="tasklist">
        <h2>Current task list</h2>

        <!-- Attribute "border" is to display borderline of table -->
        <!-- border="0": Not displayed, "1": displayed -->
        <!-- If you want to make line bolder, you can set bigger than "1" -->
        <!-- You can change the style of line by setting "border-collapse" -->
        <!-- Borderline can be changed as single line by setting to "collapse" -->
        <table border="1" style="border-collapse:collapse;">
            <!-- "tr" define row of table -->
            <thead> <!-- "thead" define header of table -->
                <tr>
                    <th class="hidden">ID</th>
                    <th>Task</th>
                    <th width="150px">Deadline</th>
                    <th width="100px">Status</th>
                    <th></th>
                </tr>
            </thead>
            <tbody> <!-- "tbody" define body of table -->
                <tr th:each="task : ${taskList}"> <!-- "th:each" define iteration -->
                    <td class="hidden" th:text="${task.id}"></td>
                    <td th:text="${task.task}"></td>
                    <td width="100px" th:text="${task.deadline}"></td>
                    <td width="50px" th:text="${task.done} ? 'Complete' : 'Incomplete'"></td>
                    <td width="50px">
                        <button type="submit" id="update_button" onclick="
                            let row = this.parentElement.parentElement;
                            getElementById('update_id').value=row.cells[0].firstChild.data;
                            getElementById('update_task').value=row.cells[1].firstChild.data;
                            getElementById('update_deadline').value=row.cells[2].firstChild.data;
                            getElementById('update_status').selectedIndex=(row.cells[3].firstChild.data=='Complete')?1:0;
                            var dialog = getElementById('updateDialog');
                            dialog.style.left = ((window.innerWidth - 500) / 2) + 'px';
                            dialog.style.display = 'block';
                        ">Update</button>
                    </td>
                    <td width="50px">
                        <form action="/delete">
                            <button type="submit" id="delete_button">Delete</button>
                            <input type="hidden" name="id" th:value="${task.id}" />
                        </form>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>

    <div id="updateDialog">
        <div class="task_form">
            <h2>Update task</h2>
            <form action="/update">
                <input id="update_id" name="id" type="hidden" />
                <label>Task</label>
                <input id="update_task" name="task" type="text" />
                <label>Deadline</label>
                <input id="update_deadline" name="deadline" type="date" />
                <label>Status</label>
                <select id="update_status" name="done">
                    <option value="false">Incomplete</option>
                    <option value="true">Complete</option>
                </select>
                <div>
                    <button type="submit">Update</button>
                    <button type="reset"
                            onclick="getElementById('updateDialog').style.display='none';">Cancel</button>
                </div>
            </form>
        </div>
    </div>
</body>

</html>

タスク一覧表示機能のエンドポイントの作成

ここで実装するコードはこのようになります。

@GetMapping("/list")
String listItems(Model model) {
    List<TaskItem> taskItems = dao.findAll();

    model.addAttribute("taskList", taskItems);

    return "home";
}

ビューを構成するHTMLはテンプレートエンジンが
作成します。そのため、このメソッドではHTTP
レスポンス本体ではなく、対応するビュー名を
文字列で返します。
ここで言うビュー名とは、HTMLテンプレートの
ファイル名から拡張子を除いたものとなります。
今回はhome.htmlというテンプレートを作成したので、
"home"をビュー名として返すようにします。

続いて引数には、org.springframework.ui.Modelクラスの
オブジェクトを渡しています。これは、Javaプログラム
とHTMLテンプレート間でのデータの受け渡しの役目を
担っています。

そして、このオブジェクトに対してaddAttributeメソッド
により属性を設定しています。これはキーと値のペアで
あり、キーはテンプレート側で使用する変数名です。
ここで実装するコードでは、キーが"taskList"、 値が
taskItems変数という属性を設定しています。

CSSによるテンプレートの装飾

HTMLテンプレートにより構成されたWebページにおける
コンテンツの色やサイズ、配置などを整えるためにCSSを
利用します。
これにより、HTMLのそれぞれの要素に対して個別に
細かなデザインを指定できるようになります。

willcloud.jp

CSSファイルや画像ファイルなどブラウザから直接
アクセスするファイルは、src/main/resources/staticの
下に配置します。ここでは、home.cssというファイル名
で下記のように作成します。

/* Common items */
body {
    margin: 30px;
    color: #283655;
}

input {
    border: 1px solid;
}
.hidden {
    display: none;
}

/* Task input form */
.task_form {
    padding: 0px 10px 10px 10px;
    border: 1px solid #4D648D;
}
.task_form form {
    display: grid;
    grid-template-columns: 100px 1fr;
}
.task_form input[type="date"], select {
    width: 150px;
}

/* Task list */
.tasklist table {
    width: 100%;
    border: 1px solid;
    border-collapse: collapse;
}

/* Dialog to update tasks */
#updateDialog {
    display: none;
    background-color: #FFFFFF;
    border: 2px double;
    width: 500px;
    position: fixed;
    top: 120px;
    z-index: 9999;
}

そして、これに伴いhome.htmlの方にもこちらの
ような記述をしておきます。こうすることで、
Webページの装飾にhome.cssを使うようになります。

<head>
    <meta charset="UTF-8">
    <title>Task List Manager</title>
    <link th:href="@{/home.css}" rel="stylesheet">
</head>

タスク情報を削除するエンドポイントの追加

Webサイトから、タスク削除のリクエストを受けられる
ようにするためのエンドポイントとなるメソッドを
実装します。

@GetMapping("/delete")
String deleteItem(@RequestParam("id") String id) {
    dao.delete(id);

    return "redirect:/list";
}

リクエストのパラメータとして受け取った
タスクのIDに応じて、それに該当するタスク
情報をデータベースから削除します。

タスク情報を更新するエンドポイントの追加

Webサイトから、タスク更新のリクエストを受けられる
ようにするためのエンドポイントとなるメソッドを
実装します。

@GetMapping("/update")
String updateItem(@RequestParam("id") String id,
                  @RequestParam("task") String task,
                  @RequestParam("deadline") String deadline,
                  @RequestParam("done") boolean done) {
    TaskItem taskItem = new TaskItem(id, task, deadline, done);

    dao.update(taskItem);

    return "redirect:/list";
}

リクエストのパラメータとして受け取ったタスクのID、
タスク内容、期限、状態の情報を引数に新たなタスク
情報オブジェクトを生成し、それをデータベースに
登録されている同IDのタスク情報に上書きします。

動作確認

ここまでのコードを実装できたら、最後にビルド->実行
して動作を確認してみましょう。アプリ実行後にWeb
ブラウザを開いて、URL入力欄にlocalhost:8080/listと
入れます。すると、このようなタスク管理アプリの
画面が開かれるはずです。

今はタスクが何も登録されていない状態なので、まずは
タスクを登録してみます。ページ上段のTask欄にタスク
内容、その下のDeadline欄に期限をそれぞれ入力し、
そのあとにRegisterボタンを押します。

すると、このようにタスクリストに登録され、Web
ページ上に表示されます。

次に、登録したタスクの内容を更新してみます。
リストに表示されているタスクの行の右端にある
Updateボタンを押します。すると、このように
タスク更新用フォームが表示されます。

ここで、タスク内容と期限をこちらのように変更
してみましょう。

変更後にUpdateボタンを押すと、このようにタスク
リストの表示も更新されるはずです。

最後に、リストからタスクを削除する機能を確認
してみます。リストに表示されているタスクの行の
右端にあるDeleteボタンを押します。

すると、このように登録されていたタスクがリスト
から削除されたのを確認できるはずです。

ここまで確認できれば、今回のアプリ開発は完了
です。お疲れ様でした。