0
0

Springboot-MVCのデモ作成

Last updated at Posted at 2024-05-04

SpringBootで、MVCモデルでアプリケーションを作成する必要が出てきたので、
練習、忘備録として記録。

formに検索条件を入力したら、DBの該当レコードを右側に表示する簡単なアプリを作成。
MybatisでのDB接続、Validationの実装など、仕事で使いそうなところを軽く確認。

Github Repository

インメモリDBを使っているので、cloneすればすぐ動作します。
一応テストコードも用意。

Controller

以下コントローラ、
詰まった点
・executeSearchメソッドにて、@Validatedアノテーションの引数の直後にBindingResultを
 セットしなければならなかった。
・executeSearchメソッドにて、BindingResultと紐づけるにはModelの属性名を
 クラス名の頭文字lowercaseで指定しなければならず、解決に時間がかかった。

package com.example.demo.search.controller;

import com.example.demo.search.model.SearchCondition;
import com.example.demo.search.model.SearchResult;
import com.example.demo.search.service.SearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
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.PostMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class SearchController {

    Logger logger = LoggerFactory.getLogger(SearchController.class);

    SearchService searchService;

    @Autowired
    public SearchController(SearchService searchService) {
        this.searchService = searchService;
    }

    @GetMapping("/search")
    public ModelAndView initPage(ModelAndView modelAndView) {
        SearchCondition condition = new SearchCondition();
        searchService.init(condition);
        //view, model
        modelAndView.setViewName("index");
        modelAndView.addObject("searchCondition", condition);
        return modelAndView;
    }

    @PostMapping("/search")
    public ModelAndView executeSearch(
    @ModelAttribute @Validated SearchCondition condition, //form要素を受け取る
    BindingResult bindingResult, //@Validatedの直後に書かないとダメ
    ModelAndView modelAndView) {
        modelAndView.setViewName("index");
        //SearchConditionの頭文字lowercaseでmodel名指定する。
        modelAndView.addObject("searchCondition", condition);

        if(bindingResult.hasErrors()){
            logger.info("Error!");
            logger.info(bindingResult.getAllErrors().toString());
            modelAndView.addObject("errorMsg", "error!");
            return modelAndView;
        }

        List<SearchResult> results = searchService.search(condition);
        modelAndView.addObject("results", results);
        return modelAndView;
    }
}

Service

特に詰まった点はなし。

package com.example.demo.search.service;

import com.example.demo.search.entity.ResultEntity;
import com.example.demo.search.model.SearchCondition;
import com.example.demo.search.model.SearchResult;
import com.example.demo.search.repository.SearchRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class SearchServiceImpl implements SearchService {

    SearchRepository searchRepository;

    @Autowired
    public SearchServiceImpl(SearchRepository repository) {
        this.searchRepository = repository;
    }

    /**
     * 初期表示時の値をセットアップする。
     *
     * @param condition condition
     */
    public void init(SearchCondition condition) {
        condition.setNameField("Yamashita");
        condition.setAgeFrom(10);
        condition.setAgeTo(20);
    }

    /**
     * 条件をもとにResultテーブルを検索する。
     *
     * @param condition condition
     * @return 検索結果のリスト
     */
    public List<SearchResult> search(SearchCondition condition) {
        List<ResultEntity> entities = searchRepository.findByDynamicCondition(condition);
        List<SearchResult> searchResults = new ArrayList<>();
        for (ResultEntity entity : entities) {
            SearchResult searchResult = new SearchResult();
            dataTransfer(searchResult, entity);
            searchResults.add(searchResult);
        }
        return searchResults;
    }

    /**
     * searchResult Entityから必要データだけを取り出す。
     *
     * @param searchResult 検索結果 画面データクラス
     * @param resultEntity  Entity
     */
    private void dataTransfer(SearchResult searchResult, ResultEntity resultEntity) {
        searchResult.setName(resultEntity.getName());
        searchResult.setAge(resultEntity.getAge());
        searchResult.setEmail(resultEntity.getEmail());
    }
}

Repository(Mybatis)

ORMは今までHibernate(JPA)しか経験がなかったが、
Hibernateよりも柔軟性があるし、何より実装がわかりやすくていいと思った。

package com.example.demo.search.repository;

import com.example.demo.search.entity.ResultEntity;
import com.example.demo.search.model.SearchCondition;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.jdbc.SQL;
import org.thymeleaf.util.StringUtils;

import java.util.List;

@Mapper
public interface SearchRepository {
    /**
     * 検索条件を元にresultテーブルからデータを取得。
     *
     * @param condition 動的検索条件
     * @return 検索結果レコードのリスト
     */
    @SelectProvider(type = SearchRepositoryProvider.class, method = "find")
    List<ResultEntity> findByDynamicCondition(SearchCondition condition);

    /**
     * 動的にSQLを生成するためのProvider
     */
    class SearchRepositoryProvider {
        /**
         * findByDynamicConditionの動的SQL生成Ï
         *
         * @param condition 検索条件クラス
         * @return sql
         */
        public String find(SearchCondition condition) {
            return new SQL() {{
                SELECT("name", "email", "age");
                FROM("result");
                if (!StringUtils.isEmpty(condition.getNameField())) {
                    WHERE("name LIKE CONCAT('%',#{nameField},'%')");
                }
                if (condition.getAgeFrom() != null) {
                    AND();
                    WHERE("age >= #{ageFrom}");
                }
                if (condition.getAgeTo() != null) {
                    AND();
                    WHERE("age <= #{ageTo}");
                }
            }}.toString();
        }
    }


}

画面Bean

Controllerでも書いたが、Validation周りが一番手間取った。
カスタムアノテーションなど使いこなせれば良さそうなので、要練習。

package com.example.demo.search.model;

import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
public class SearchCondition {
    String nameField;

    Integer ageFrom;

    Integer ageTo;

    @AssertTrue(message = "From, Toの大小関係が不正です。")
    public boolean isFromToValidate(){
        if(ageFrom != null && ageTo != null){
            return this.ageFrom <= this.ageTo;
        }
        return true;
    }

    @AssertTrue(message = "検索条件を入力してください。")
    public boolean isAllInputCheck(){
        return (this.nameField != null && !this.nameField.isEmpty())
                || this.ageFrom != null || this.ageTo != null;
    }
}

画面HTML

特にないが、強いていうならエラー周りの処理はもう少し勉強したい。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="jp">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link th:href="@{/css/index.css}" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<p>Hello!</p>
<div class="article">
    <div class="side">

        <div class="condition">
            <form class="w-full max-w-sm bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" th:action="@{/search}" method="post" th:object="${searchCondition}">
                <div class="md:flex md:items-center mb-6">
                    <div class="md:w-1/3">
                        <label class="block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4" >
                            name
                        </label>
                    </div>
                    <div class="md:w-2/3">
                        <input class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500" th:value="*{nameField}" name="nameField" type="text">
                    </div>
                </div>
                <div class="md:flex md:items-center mb-6">
                    <div class="md:w-1/3">
                        <label class="block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4">
                            age from
                        </label>
                    </div>
                    <div class="md:w-2/3">
                        <input class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500" th:value="*{ageFrom}" name="ageFrom" type="number">
                    </div>
                </div>
                <div class="md:flex md:items-center mb-6">
                    <div class="md:w-1/3">
                        <label class="block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4">
                            age to
                        </label>
                    </div>
                    <div class="md:w-2/3">
                        <input class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500" th:value="*{ageTo}" name="ageTo" type="number">
                    </div>
                </div>
                <div class="md:flex md:items-center">
                    <div class="md:w-1/3"></div>
                    <div class="md:w-2/3">
                        <button class="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded" type="submit">
                            Submit
                        </button>
                    </div>
                </div>

                <div th:if="${#arrays.length(#fields.detailedErrors())} > 0" class="row">
                    <div class="col-md-12 m-3">
                        <div class="p-3 mb-2 bg-danger">
                            <div th:each="error : ${#fields.detailedErrors()}">
                                <span th:text="${error.message}"/>
                            </div>
                        </div>
                    </div>
                </div>
            </form>
        </div>
    </div>
    <div class="content">


        <div class="result">

            <div class="relative overflow-x-auto shadow-md sm:rounded-lg">
                <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
                    <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                    <tr>
                        <th scope="col" class="px-6 py-3">
                            name
                        </th>
                        <th scope="col" class="px-6 py-3">
                            email
                        </th>
                        <th scope="col" class="px-6 py-3">
                            age
                        </th>

                    </tr>
                    </thead>
                    <tbody>
                    <tr class="text-xl odd:bg-white odd:dark:bg-gray-900 even:bg-gray-100 even:dark:bg-gray-800 border-b dark:border-gray-700" th:each="result : ${results}">
                        <td th:text="${result.name}"></td>
                        <td th:text="${result.email}"></td>
                        <td th:text="${result.age}"></td>
                    </tr>

                    </tbody>
                </table>
            </div>

        </div>
    </div>
    <div></div>

</div>

</body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0