【Java】Spring Boot 3によるWebアプリ開発入門 ~Step3:クイズアプリ実装~

1. はじめに

本シリーズではSpring Boot3を用いてクイズアプリの開発する方法をご紹介していきます。ひととおり実践して基礎を理解してもらえれば、自分が作りたいWebアプリを開発することができると思いますので、ぜひ挑戦してみてください。下記のStep1の環境構築、Step2のPostgresデータベースの構築に続いて、Step3でクイズ一覧画面を表示するアプリケーションを作成していきます。
【Java】Spring Boot 3によるWebアプリ開発入門 ~Step1:環境構築~
【Java】Spring Boot 3によるWebアプリ開発入門~Step2:データベース構築~

2. Spring MVCを適用したアーキテクチャ概要

本アプリではSpring MVC(Model、View、Controller)というフレームワークを適用しています。こういったフレームワークを使うことで、アプリ開発を効率的かつ柔軟に進めることが可能となります。下記に概念図を記載します。

処理の大まかな流れをご説明します。

  1. ブラウザからアプリケーションにアクセスすると、HTTPリクエストがMVCモデルのCに該当するControllerに渡されます。
  2. Controllerはリクエストに応じて適切なビジネスロジックに処理を振り分ける役割を持っております。本アプリではアクセスしたURLに応じて業務ロジックを実行する適切なServceを呼び出します。
  3. データベースに対するCRUD処理を行うために、ServiceからRepositoryが呼び出されます。
  4. Repositoryがデータベースに対して必要なCRUD処理を行います。今回ご紹介するアプリおいて、Postgresデータベースに対する処理はSpring Data JPAというライブラリを用いて実装しています。
  5. データベースに対するCRUD処理の結果がRepositoryに返却されます。
  6. Repository処理の結果がServiceに返却されます。
  7. Service処理の結果がControllerに返却されます。
  8. Controllerの指示で、画像データを生成します。今回ご紹介するアプリにおいては、Thymeleafというテンプレートエンジンを用いてViewを作成しています。
  9. Viewで作成した画像データをブラウザに返却します。

※Entityはデータベースのテーブル構造を表したオブジェクトであり、Controller、Service、Repositoryから適宜呼び出されます。

3. pom.xmlの設定

pom.xmlとはJavaプロジェクトのビルド、管理、依存関係の解決を行うMavenの設定ファイルです。Spring Data JPA、thymeleaf、postgresを使用するための設定を追加しているので参考にしてみてください。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.eeengineer.springboot</groupId>
	<artifactId>quizapp</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>quizapp</name>
	<description>quizapp project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

4. データベース接続の設定

src/main/resources配下にapplication.propertiesを作成し、postgresのquizデータベースに接続するための設定を記載します。

#
# JDBC properties
#
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/quiz
spring.datasource.username=postgres
spring.datasource.password=password

5. Controllerの実装

それでは早速、Controllerから作成してきましょう。機能としては大きく3つあります。

  1. クイズリストの表示(@GetMapping(“/list”))
    “/quiz/list”にアクセスにアクセスすることで、データベースからクイズの一覧を取得する。
  2. クイズ表示(@GetMapping(“/list/{quizid}”))
    クイズリストにおいて該当クイズを選択すると、quizidに応じた実際のクイズ画面に遷移する。
  3. クイズステータス変更(@GetMapping(“/list/toggleStatus”))
    特定のクイズのステータス(完了 or 未完了)を変更する。
package eeengineer.quizapp.controller;

import eeengineer.quizapp.entity.Quiz;
import eeengineer.quizapp.service.QuizService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@Controller
@RequestMapping("/quiz")
public class QuizController {

    private QuizService quizService;

    public QuizController(QuizService theQuizService) {
        quizService = theQuizService;
    }

    @GetMapping("/list")
    public String listQuiz(Model theModel) {

        // get the quiz from db
        List theQuiz = quizService.findAll();

        // add to the spring model
        theModel.addAttribute("quiz", theQuiz);

        return "list-quiz";
    }

    @GetMapping("/list/{quizid}")
    public String QuizByID(@PathVariable Integer quizid, Model theModel) {

        Quiz theQuiz = quizService.findById(quizid);

        // add to the spring model
        theModel.addAttribute("quiz", theQuiz);

        return "byid-quiz";
    }

    @GetMapping("/list/toggleStatus")
    public String changeStatus(Integer quizid) {
        quizService.changeStatus(quizid);
        return "redirect:/";
    }

}

6. ModelにおけるEntityの実装

Quizテーブルのテーブル構造を表すEntityを作成していきます。データの属性を取得および値を入力するためのgetter、setterメソッドを記載します。

package eeengineer.quizapp.entity;

import jakarta.persistence.*;

@Entity
@Table(name="quiz")
public class Quiz {

    // define fields
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="quizid")
    private int quizid;
    
    @Column(name="userid")
    private int userid;

    @Column(name="quiz")
    private String quiz;

    @Column(name="category")
    private String category;

    @Column(name="option1")
    private String option1;

    @Column(name="option2")
    private String option2;

    @Column(name="option3")
    private String option3;

    @Column(name="option4")
    private String option4;

    @Column(name="answer")
    private int answer;

    @Column(name="explain")
    private String explain;

    @Column(name="status")
    private String status;

    // define constructors
    public Quiz() {

    }

    public Quiz(int quizid, int userid,String quiz, String category, String option1, String option2, String option3, String option4, Integer answer, String explain, String status) {
        this.quizid = quizid;
        this.userid = userid;
        this.quiz = quiz;
        this.category = category;
        this.option1 = option1;
        this.option2 = option2;
        this.option3 = option3;
        this.option4 = option4;
        this.answer = answer;
        this.explain = explain;
        this.status = status;
    }

    public int getQuizid() {
        return quizid;
    }
    
    public Integer getUserid() {
        return userid;
    }

    public String getQuiz() {
        return quiz;
    }

    public String getCategory() {
        return category;
    }

    public String getOption1() {
        return option1;
    }

    public String getOption2() {
        return option2;
    }

    public String getOption3() {
        return option3;
    }

    public String getOption4() {
        return option4;
    }

    public Integer getAnswer() {
        return answer;
    }

    public String getExplain() {
        return explain;
    }

    public String getStatus() {
        return status;
    }

    public void setQuizid(int quizid) {
        this.quizid = quizid;
    }
    
    public void setUserid(int userid) {
        this.userid = userid;
    }

    public void setQuiz(String quiz) {
        this.quiz = quiz;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public void setOption1(String option1) {
        this.option1 = option1;
    }

    public void setOption2(String option2) {
        this.option2 = option2;
    }

    public void setOption3(String option3) {
        this.option3 = option3;
    }

    public void setOption4(String option4) {
        this.option4 = option4;
    }

    public void setAnswer(Integer answer) {
        this.answer = answer;
    }

    public void setExplain(String explain) {
        this.explain = explain;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    // define toString
    @Override
    public String toString() {
        return "Qiuz{" +
                "quizid=" + quizid +
                ", category='" + category + '\'' +
                ", quiz='" + quiz + '\'' +
                ", status='" + status + '\'' +
                '}';
    }
}

7. ModelにおけるServiceの実装

続いてControllerから呼び出されるServiceを実装していきます。インターフェースとしてquizServiceを作成し、具体的な実装はquizServiceImplに記載していきます。以下の3つの機能を実装しています。

  1. クイズリストの取得(findAll())
    データベースからクイズの一覧を取得する。
  2. クイズ取得(findById(int theId)
    データベースからquizidに対応したクイズの情報を取得する。
  3. クイズステータス変更(changeStatus(int theId))
    quizidに対応したクイズのステータス(完了 or 未完了)を変更する。
package eeengineer.quizapp.service;

import eeengineer.quizapp.entity.Quiz;
import java.util.List;

public interface QuizService {

    List findAll();

    Quiz findById(int theId);

    void changeStatus(int theId);

}
package eeengineer.quizapp.service;

import eeengineer.quizapp.repository.QuizRepository;
import eeengineer.quizapp.entity.Quiz;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class QuizServiceImpl implements QuizService {

    private QuizRepository quizRepository;

    public QuizServiceImpl(QuizRepository theQuizRepository) {
        quizRepository = theQuizRepository;
    }

    @Override
    public List<Quiz> findAll() {
        return quizRepository.findAll();
    }

    @Override
    public Quiz findById(int theId) {
        Optional<Quiz> result = quizRepository.findById(theId);

        Quiz theQuiz = null;

        if (result.isPresent()) {
            theQuiz = result.get();
        } else {
            // we didn't find the quiz
            throw new RuntimeException("Did not find quiz id - " + theId);
        }

        return theQuiz;
    }

    @Override
    public void changeStatus(int theId) {
        @SuppressWarnings("deprecation")
		Quiz theQuiz = quizRepository.getById(theId);
        String quizStatus = theQuiz.getStatus();
        String changeStatus = "";
        if(quizStatus.equals("完了")) {
            changeStatus = "未完了";
        }
        else{
            changeStatus = "完了";
        }
        theQuiz.setStatus(changeStatus);
        quizRepository.save(theQuiz);
    }
}

8. ModelにおけるRepositoryの実装

続いてServiceから呼び出されるRepositoryを作っていきます。Spring Data JPAを用いているため、JpaRepositoryを継承したインターフェースを作成するだけで大丈夫です。

package eeengineer.quizapp.repository;

import eeengineer.quizapp.entity.Quiz;
import org.springframework.data.jpa.repository.JpaRepository;

public interface QuizRepository extends JpaRepository<Quiz, Integer> {

    // that's it ... no need to write any code LOL!

}

9. Viewの実装

src/main/resources/templates配下に以下の2つのhtmlファイルを作成します。

1. list-quiz.html
クイズのリストを表示するhtmlを作成します。クイズのカテゴリ、クイズの内容に加えて、実際のクイズに進むためのリンク、クイズのステータス(完了 or 未完了)を表示させてています。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ITエンジニア育成クイズ</title>

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>

<div class="container">

    <h3>ITエンジニア育成クイズ</h3>
    <hr>

    <table class="table table-bordered table-striped">
        <thead class="table-dark">
            <tr>
                <th>クイズID</th>
                <th>カテゴリ</th>
                <th>クイズ</th>
                <th>リンク</th>
                <th>ステータス</th>
            </tr>
        </thead>

        <tbody>
            <tr th:each="tempQuiz : ${quiz}">
                <td th:text="${tempQuiz.quizid}" />
                <td th:text="${tempQuiz.category}" />
                <td th:text="${tempQuiz.quiz}"/>
                <td>
                    <a th:href="@{'./list/'+ ${tempQuiz.quizid}}">クイズへ</a>
                </td>
                <td>
                    <span th:if="${tempQuiz.status}=='完了'" th:text="完了"></span>
                    <span th:if="${tempQuiz.status}=='未完了'" th:text="未完了"></span>
                </td>
            </tr>
        </tbody>
    </table>

</div>

</body>
</html>

2. byid-quiz.html
実際のクイズ出題画面を作成します。クイズおよび4つの選択肢、解答および完了/未完了のステータスを変更するボタンをつけています。
また、一番下にはクイズ一覧に戻るためのリンクも付けています。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ITエンジニア育成クイズ</title>

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>

<div class="container" style="margin-top: 30pt;">
    <tr th:each="tempQuiz : ${quiz}">

        <h3>問題</h3>

        <p><td th:text="${tempQuiz.quiz}"></p>

        <p><span style="font-weight: bold;">1.</span>
            <span th:text="${tempQuiz.option1}"></span></p>
        <p><span style="font-weight: bold;">2.</span>
            <span th:text="${tempQuiz.option2}"></span></p>
        <p><span style="font-weight: bold;">3.</span>
            <span th:text="${tempQuiz.option3}"></span></p>
        <p><span style="font-weight: bold;">4.</span>
            <span th:text="${tempQuiz.option4}"></span></p>

        <p><details>
          <summary>解答(クリック)</summary>
            <p th:text="${tempQuiz.answer}"></p>
            <p th:text="${tempQuiz.explain}"></p>
        </details></p>

        <p><form action="/quiz/list/toggleStatus" method="get">
            <input type="hidden" name="quizid" th:value="${tempQuiz.quizid}"/>
            <button type="submit" name="incomplete" th:if="${tempQuiz.status}=='完了'">未完了にする</button>
            <button type="submit" name="complete" th:if="${tempQuiz.status}=='未完了'">完了にする</button>
        </form></p>

        <p><a th:href="@{/quiz/list}">クイズ一覧に戻る</a></p>

    </tr>

</div>
</body>
</html>

10. クイズアプリの動作

クイズアプリに必要なソースコードは以上です。それでは最後に動作確認をしてみましょう。表示させたいクイズをInsert文でpostgresに登録したうえで、アプリを起動してhttp://localhost:8080/quiz/listにアクセスしてみてください。下記の通りクイズのリストが表示されたら成功です。

続いてクイズへのリンクをクリックしてください。下記の通り実際のクイズが表示されます。

解答を確認したい場合は、「解答(クリック)」をクリックすると、下図のように解答が表示されます。

このクイズを完了ステータスに変えたい場合、上図の赤枠の「完了にする」をクリックすると、クイズ一覧の画面に戻ります。
該当のクイズが完了ステータスになっていることが確認できるかと思います。

11. まとめ

Spring Bootを適用したクイズアプリ作成の説明は以上となります。フレームワークを用いることで、思ったより簡単にデータベースのCRUD操作および画面表示ができることがわかっていただけたのではないかと思います。下記のGithubにサンプルコードを共有してますので、参考にしてみてください。
https://github.com/eeengineering/quizapp-step3

Step4ではSpring Securityを適用したログイン機能、およびユーザごとのクイズステータス管理の機能を実装しますので、引き続き参考にしてみて下さい。
【Java】Spring Boot 3によるWebアプリ開発入門 ~Step4:Spring Securityによるログイン機能~

11. 参考文献

コメント