Spring Boot DB連携

Spring BootのWEB画面上で、MyBatisによるOracle接続を実装してみた

今回は、Oracle接続する処理をMyBatisにより実装してみたので、そのサンプルプログラムを共有する。MyBatisを利用すると、SQL文をそのまま利用することができるため、これまでのJPAと比べ、複数のテーブル結合を含む複雑なSQL文に対しても容易に対応できる。

前提条件

下記サイトのソースコード実装が完了していること。

Spring BootのWEB画面上でCRUDを含むOracleアクセス処理を実装してみた(ソースコード編)今回も、C(Create)・R(Read)・U(Update)・D(Delete)を一通り含むOracle接続処理を含むサンプルプログラ...

また、テーブル「m_sex」を新規作成していること。実行したSQLは以下の通り。

create table m_sex (
    sex_cd char(1) primary key not null,
    sex_value varchar2(4) NOT NULL
);
insert into m_sex values (1, '男');
insert into m_sex values (2, '女');
commit;

上記SQLを実行した結果は下図の通り。
m_sex_テーブルデータ

完成した画面イメージ

下記サイトの「完成した画面イメージの共有」を参照のこと。

Spring BootのWEB画面上でCRUDを含むOracleアクセス処理を実装してみた(完成イメージ編)今回は、C(Create)・R(Read)・U(Update)・D(Delete)を一通り含むOracle接続処理をSpring Boo...

作成したサンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。「前提条件」で記載したソースコードと異なるプログラムを赤枠で囲っている。
サンプルプログラムの構成

「build.gradle」は下記の通りで、「mybatis-spring-boot-starter」を追加している。

plugins {
	id 'org.springframework.boot' version '2.1.7.RELEASE'
	id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	compileOnly 'org.projectlombok:lombok:1.18.10'
	annotationProcessor 'org.projectlombok:lombok:1.18.10'
	compile files('lib/ojdbc6.jar')
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1'
}

また、「UserData.java」は下記の通りで、USER_DATAテーブルの各項目値と、性別(文字列)を指定している。
性別(文字列)は、M_SEXテーブルから取得したSEX_VALUEの値が入る。

package com.example.demo;

import lombok.Data;

/**
 * ユーザーデータテーブル(user_data)アクセス用エンティティ
 */
@Data
public class UserData {

    /** ID */
    private long id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private int birthY;

    /** 生年月日_月 */
    private int birthM;

    /** 生年月日_日 */
    private int birthD;

    /** 性別 */
    private String sex;

    /** 性別(文字列) */
    private String sex_value;
}



また、「UserDataMapper.java」は下記の通りで、USER_DATAテーブルとアクセスする処理を記載している。「@Mapper」「@Select」「@Insert」「@Delete」「@Update」の各アノテーションを利用している。さらに、findAllメソッドでは、2テーブルを結合した結果を取得している。

package com.example.demo;

import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Update;
import java.util.Collection;

@Mapper
public interface UserDataMapper {

    /**
     * ユーザーデータテーブル(user_data)を全件取得する
     * @return ユーザーデータテーブル(user_data)を全データ
     */
    @Select("SELECT u.id, u.name, u.birth_year as birthY, u.birth_month as birthM"
            + ", u.birth_day as birthD, u.sex as sex, m.sex_value as sex_value "
            + " FROM USER_DATA u, M_SEX m WHERE u.sex = m.sex_cd ORDER BY u.id ")
    Collection<UserData> findAll();

    /**
     * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する
     * @param id ID
     * @return ユーザーデータテーブル(user_data)の指定したIDのデータ
     */
    @Select("SELECT id, name, birth_year as birthY, birth_month as birthM"
            + ", birth_day as birthD, sex FROM USER_DATA WHERE id = #{id}")
    UserData findById(Long id);

    /**
     * 指定したIDをもつユーザーデータテーブル(user_data)のデータを削除する
     * @param id ID
     */
    @Delete("DELETE FROM USER_DATA WHERE id = #{id}")
    void deleteById(Long id);

    /**
     * 指定したユーザーデータテーブル(user_data)のデータを追加する
     * @param userData ユーザーデータテーブル(user_data)の追加データ
     */
    @Insert("INSERT INTO USER_DATA VALUES (#{id}, #{name}, #{birthY}"
            + ", #{birthM}, #{birthD}, #{sex})")
    void create(UserData userData);

    /**
     * 指定したユーザーデータテーブル(user_data)のデータを更新する
     * @param userData ユーザーデータテーブル(user_data)の更新データ
     */
    @Update("UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}"
            + ", birth_month = #{birthM}, birth_day = #{birthD}"
            + ", sex = #{sex} WHERE id = #{id}")
    void update(UserData userData);

    /**
     * ユーザーデータテーブル(user_data)の最大値IDを取得する
     * @return ユーザーデータテーブル(user_data)の最大値ID
     */
    @Select("SELECT NVL(max(id), 0) as maxId FROM USER_DATA")
    long findMaxId();
}



さらに、「DemoForm.java」は、以下のように、性別(文字列)を追加している。

package com.example.demo;

import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Formオブジェクトのクラス
 */
@Data
public class DemoForm {

    /** ID */
    private String id;

    /** 名前 */
    @NotEmpty
    private String name;

    /** 生年月日_年 */
    private String birthYear;

    /** 生年月日_月 */
    private String birthMonth;

    /** 生年月日_日 */
    private String birthDay;

    /** 性別 */
    @NotEmpty
    private String sex;

    /** 確認チェック */
    @NotEmpty
    private String checked;

    /** 性別(文字列) */
    private String sex_value;

    /** 生年月日_月のMapオブジェクト */
    public Map<String,String> getMonthItems(){
        Map<String, String> monthMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 12; i++){
            monthMap.put(String.valueOf(i), String.valueOf(i));
        }
        return monthMap;
    }

    /** 生年月日_日のMapオブジェクト */
    public Map<String,String> getDayItems(){
        Map<String, String> dayMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 31; i++){
            dayMap.put(String.valueOf(i), String.valueOf(i));
        }
        return dayMap;
    }

    /** 性別のMapオブジェクト */
    public Map<String,String> getSexItems(){
        Map<String, String> sexMap = new LinkedHashMap<String, String>();
        sexMap.put("1", "男");
        sexMap.put("2", "女");
        return sexMap;
    }

}



また、「UserDataMapper.java」を呼び出すコントローラ処理「DemoController.java」は下記の通り。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.support.SessionStatus;

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

@Controller
@SessionAttributes(types = {DemoForm.class})
public class DemoController {

    /**
     * ユーザーデータテーブル(user_data)へアクセスするマッパー
     */
    @Autowired
    private UserDataMapper mapper;

    /**
     * ユーザーデータテーブル(user_data)のデータを取得して返却する
     * @return ユーザーデータリスト
     */
    @ModelAttribute("demoFormList")
    public List<DemoForm> userDataList(){
        List<DemoForm> demoFormList = new ArrayList<>();
        return demoFormList;
    }

    /**
     * Formオブジェクトを初期化して返却する
     * @return Formオブジェクト
     */
    @ModelAttribute("demoForm")
    public DemoForm createDemoForm(){
        DemoForm demoForm = new DemoForm();
        return demoForm;
    }

    /**
     * 初期表示(一覧)画面に遷移する
     * @param model Modelオブジェクト
     * @return 一覧画面へのパス
     */
    @RequestMapping("/")
    public String index(Model model){
        List<DemoForm> demoFormList = new ArrayList<>();
        //ユーザーデータテーブル(user_data)から全データを取得する
        Collection<UserData> userDataList = mapper.findAll();
        for(UserData userData : userDataList){
            demoFormList.add(getDemoForm(userData));
        }
        //ユーザーデータリストを更新
        model.addAttribute("demoFormList", demoFormList);
        return "list";
    }

    /**
     * 更新処理を行う画面に遷移する
     * @param id 更新対象のID
     * @param model Modelオブジェクト
     * @return 入力・更新画面へのパス
     */
    @GetMapping("/update")
    public String update(@RequestParam("id") String id, Model model){
        UserData userData = mapper.findById(Long.parseLong(id));
        model.addAttribute("demoForm", getDemoForm(userData));
        return "input";
    }

    /**
     * 削除確認画面に遷移する
     * @param id 更新対象のID
     * @param model Modelオブジェクト
     * @return 削除確認画面へのパス
     */
    @GetMapping("/delete_confirm")
    public String delete_confirm(@RequestParam("id") String id, Model model){
        UserData userData = mapper.findById(Long.parseLong(id));
        model.addAttribute("demoForm", getDemoForm(userData));
        return "confirm_delete";
    }

    /**
     * 削除処理を行う
     * @param demoForm Formオブジェクト
     * @return 一覧画面の表示処理
     */
    @PostMapping(value = "/delete", params = "next")
    @Transactional(readOnly = false)
    public String delete(DemoForm demoForm){
        mapper.deleteById(Long.parseLong(demoForm.getId()));
        return "redirect:/to_index";
    }

    /**
     * 削除完了後に一覧画面に戻る
     * @param model Modelオブジェクト
     * @return 一覧画面
     */
    @GetMapping("/to_index")
    public String toIndex(Model model){
        return index(model);
    }

    /**
     * 削除確認画面から一覧画面に戻る
     * @param model Modelオブジェクト
     * @return 一覧画面
     */
    @PostMapping(value = "/delete", params = "back")
    public String confirmDeleteBack(Model model){
        return index(model);
    }

    /**
     * 追加処理を行う画面に遷移する
     * @param model Modelオブジェクト
     * @return 入力・更新画面へのパス
     */
    @PostMapping("/add")
    public String add(Model model){
        model.addAttribute("demoForm", new DemoForm());
        return "input";
    }

    /**
     * エラーチェックを行い、エラーが無ければ確認画面に遷移し、
     * エラーがあれば入力画面のままとする
     * @param demoForm Formオブジェクト
     * @param result バインド結果
     * @return 確認画面または入力画面へのパス
     */
    @PostMapping(value = "/confirm", params = "next")
    public String confirm(@Validated DemoForm demoForm, BindingResult result){
        //formオブジェクトのチェック処理を行う
        if(result.hasErrors()){
            //エラーがある場合は、入力画面のままとする
            return "input";
        }
        //アノテーション以外のチェック処理を行い、画面遷移する
        return checkOthers(demoForm, result, "confirm");
    }

    /**
     * 一覧画面に戻る
     * @param model Modelオブジェクト
     * @return 一覧画面の表示処理
     */
    @PostMapping(value = "/confirm", params = "back")
    public String confirmBack(Model model){
        return index(model);
    }

    /**
     * 完了画面に遷移する
     * @param demoForm Formオブジェクト
     * @param result バインド結果
     * @return 完了画面
     */
    @PostMapping(value = "/send", params = "next")
    @Transactional(readOnly = false)
    public String send(@Validated DemoForm demoForm, BindingResult result){
        //formオブジェクトのチェック処理を行う
        if(result.hasErrors()){
            //エラーがある場合は、入力画面のままとする
            return "input";
        }
        //アノテーション以外のチェック処理を行い、
        //エラーがなければ、更新・追加処理を行う
        String normalPath = "redirect:/complete";
        String checkOthersPath = checkOthers(demoForm, result, normalPath);
        if(normalPath.equals(checkOthersPath)){
            //更新・追加処理を行うエンティティを生成
            UserData userData = getUserData(demoForm);
            //追加・更新処理
            if(demoForm.getId() == null){
                userData.setId(mapper.findMaxId() + 1);
                mapper.create(userData);
            }else{
                mapper.update(userData);
            }
        }
        return checkOthersPath;
    }

    /**
     * 完了画面に遷移する
     * @param sessionStatus セッションステータス
     * @return 完了画面
     */
    @GetMapping("/complete")
    public String complete(SessionStatus sessionStatus){
        //セッションオブジェクトを破棄
        sessionStatus.setComplete();
        return "complete";
    }

    /**
     * 入力画面に戻る
     * @return 入力画面
     */
    @PostMapping(value = "/send", params = "back")
    public String sendBack(){
        return "input";
    }

    /**
     * アノテーション以外のチェック処理を行い、画面遷移先を返却
     * @param demoForm Formオブジェクト
     * @param result バインド結果
     * @param normalPath 正常時の画面遷移先
     * @return 確認画面または入力画面へのパス
     */
    private String checkOthers(DemoForm demoForm, BindingResult result
            , String normalPath){
        //** アノテーション以外のチェック処理を行う
        //** エラーがある場合は、エラーメッセージ・(エラー時に赤反転するための)
        //** エラーフィールドの設定を行い、入力画面のままとする
        //生年月日のチェック処理
        int checkDate = DateCheckUtil.checkDate(demoForm.getBirthYear()
                , demoForm.getBirthMonth(), demoForm.getBirthDay());
        switch(checkDate){
            case 1:
                //生年月日_年が空文字の場合のエラー処理
                result.rejectValue("birthYear", "validation.date-empty"
                        , new String[]{"生年月日_年"}, "");
                return "input";
            case 2:
                //生年月日_月が空文字の場合のエラー処理
                result.rejectValue("birthMonth", "validation.date-empty"
                        , new String[]{"生年月日_月"}, "");
                return "input";
            case 3:
                //生年月日_日が空文字の場合のエラー処理
                result.rejectValue("birthDay", "validation.date-empty"
                        , new String[]{"生年月日_日"}, "");
                return "input";
            case 4:
                //生年月日の日付が不正な場合のエラー処理
                result.rejectValue("birthYear", "validation.date-invalidate");
                //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、
                //メッセージを空文字に設定している
                result.rejectValue("birthMonth", "validation.empty-msg");
                result.rejectValue("birthDay", "validation.empty-msg");
                return "input";
            case 5:
                //生年月日の日付が未来日の場合のエラー処理
                result.rejectValue("birthYear", "validation.date-future");
                //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、
                //メッセージを空文字に設定している
                result.rejectValue("birthMonth", "validation.empty-msg");
                result.rejectValue("birthDay", "validation.empty-msg");
                return "input";
            default:
                //性別が不正に書き換えられていないかチェックする
                if(!demoForm.getSexItems().keySet().contains(demoForm.getSex())){
                    result.rejectValue("sex", "validation.sex-invalidate");
                    return "input";
                }
                //エラーチェックに問題が無いので、正常時の画面遷移先に遷移
                return normalPath;
        }
    }

    /**
     * DemoFormオブジェクトに引数のユーザーデータの各値を設定する
     * @param userData ユーザーデータ
     * @return DemoFormオブジェクト
     */
    private DemoForm getDemoForm(UserData userData){
        DemoForm demoForm = new DemoForm();
        demoForm.setId(String.valueOf(userData.getId()));
        demoForm.setName(userData.getName());
        demoForm.setBirthYear(String.valueOf(userData.getBirthY()));
        demoForm.setBirthMonth(String.valueOf(userData.getBirthM()));
        demoForm.setBirthDay(String.valueOf(userData.getBirthD()));
        demoForm.setSex(userData.getSex());
        demoForm.setSex_value(userData.getSex_value());
        return demoForm;
    }

    /**
     * UserDataオブジェクトに引数のフォームの各値を設定する
     * @param demoForm DemoFormオブジェクト
     * @return ユーザーデータ
     */
    private UserData getUserData(DemoForm demoForm){
        UserData userData = new UserData();
        if(!DateCheckUtil.isEmpty(demoForm.getId())){
            userData.setId(Long.valueOf(demoForm.getId()));
        }
        userData.setName(demoForm.getName());
        userData.setBirthY(Integer.valueOf(demoForm.getBirthYear()));
        userData.setBirthM(Integer.valueOf(demoForm.getBirthMonth()));
        userData.setBirthD(Integer.valueOf(demoForm.getBirthDay()));
        userData.setSex(demoForm.getSex());
        userData.setSex_value(demoForm.getSex_value());
        return userData;
    }

}



また、「list.html」は以下の通りで、sex_valueの値を直接表示するようにしている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
ユーザーデータテーブル(user_data)の全データ<br/><br/>

<table border="1" cellpadding="5">
    <tr>
        <th>ID</th>
        <th>名前</th>
        <th>生年月日</th>
        <th>性別</th>
        <th></th>
        <th></th>
    </tr>
    <tr th:each="obj : ${demoFormList}">
        <td th:text="${obj.id}"></td>
        <td th:text="${obj.name}"></td>
        <td th:text="|${obj.birthYear}年 ${obj.birthMonth}月 ${obj.birthDay}日|">
        </td>
        <td th:text="${obj.sex_value}"></td>
        <td><a href="/update"
               th:href="@{/update(id=${'__${obj.id}__'})}">更新</a></td>
        <td><a href="/delete_confirm"
               th:href="@{/delete_confirm(id=${'__${obj.id}__'})}">削除</a></td>
    </tr>
</table>
<br/><br/>
<form method="post" th:action="@{/add}">
    <input type="submit" value="データ追加" /><br/><br/>
    <input type="button" value="閉じる" onclick="window.close();" />
</form>
</body>
</html>

さらに、「application.properties」は以下の通りで、下2行でSQLログ出力の定義を行っている。

server.port = 8084
# DB接続情報
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=USER01
spring.datasource.password=USER01
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
# SQLログ出力
logging.level.org.springframework=warn
logging.level.com.example.demo.UserDataMapper=debug

上記「application.properties」によるコンソールログ出力例は以下の通り。
mybatis_コンソールログ

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-mybatis/demo

要点まとめ

  • MyBatisを利用すると、複雑なSQL文に対しても容易に対応できる。
  • MyBatisを利用できるようにするには、build.gradleに「mybatis-spring-boot-starter」を追加する。
  • エンティティクラスには、テーブルの各項目値を指定すればよい。
  • テーブルにアクセスするMapperクラスは、クラスに「@Mapper」を付与し、各メソッドに「@Select」「@Insert」「@Delete」「@Update」のいずれかを付与すればよい。
  • SQLログ・バインドパラメータの出力定義は、application.propertiesに記載する。