概要
これまでJdbcTemplateの使用経験しかなくMyBatisを使ったことがなかったので、SpringBoot + MyBatis + MySQLを使用してみた。
環境
- macOS Catalina 10.15.7
- Java11
- SpringBoot 2.3.4
- MySQL 8.0
- Gradle
##やってみたこと
今回は本の情報がDBに登録されており、それを1件取得、全件表示することにします。
- データベースからID検索し表示
- 全件取得して一覧表示
準備する各種クラス・設定ファイルたち
- build.gradle(こちらの依存関係にMyBatisを追加)
- DB関連:data.sql、schema.sql
- BookFormクラス(入力フォームからの値を受け取る)
- ビュー画面(Thymeleaf使用)
- Bookクラス(DBからの値を受け取るEntityクラス)
- BookDaoインタフェース(DBへ問合せを行うためのインタフェース)
- BookDao.xml(ここにSQLを記載。)
- BookServiceクラス(Daoクラスを呼び出す。)
- BookControllerクラス(ブラウザからのリクエストに応じたビューを返す。状況に応じてserviceクラスを呼び出す。)
なぜか、BookDaoはインタフェースを実装せずともBookServiceクラスから利用できる。裏側でごにょごにょ実装されてServiceクラスから利用できるようになっているらしい。
ディレクトリ構成
- MyBatisはマッピングファイル(XMLファイル)にSQLを書くやり方を採用(アノテーション内にSQLを書くやり方もあり)。マッピングファイルはresorces配下のDaoと同じ階層に配置することで、Dao利用時に自動で読み込んでくれる。
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── ServletInitializer.java
│ │ ├── SpringTestApplication.java
│ │ ├── controller
│ │ │ └── BookController.java
│ │ ├── dao
│ │ │ └── BookDao.java
│ │ ├── entity
│ │ │ └── Book.java
│ │ ├── form
│ │ │ └── BookForm.java
│ │ └── service
│ │ └── BookService.java
│ └── resources
│ ├── application.properties
│ ├── com
│ │ └── example
│ │ └── demo
│ │ └── dao
│ │ └── BookDao.xml
│ ├── data.sql
│ ├── schema.sql
│ ├── static
│ │ └── css
│ │ └── style.css
│ └── templates
│ └── index.html
└── test
依存関係
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
DBの定義
まずはMySQLでデータベースを作っておく。今回の例でいうとlibrary。
CREATE DATABASE library;
下記ファイルを用意すると、SpringBoot起動の度にテストデータを用意してくれる。
--booktableがあれば削除
DROP TABLE IF EXISTS booktable;
--booktableがなければ新しく作成
CREATE TABLE IF NOT EXISTS booktable(
id INT AUTO_INCREMENT,
book_name VARCHAR(50) NOT NULL,
volume_num INT NOT NULL,
author_name VARCHAR(50) NOT NULL,
published_date DATE NOT NULL,
PRIMARY KEY(id)
);
--本のリスト初期データ
--idカラムはオートインクリメントなので不要
INSERT INTO booktable
(book_name, volume_num,author_name,published_date)
VALUES
( 'HUNTER X HUNTER',36,'冨樫義博','2018-10-04'),
( 'ベルセルク',40,'三浦健太郎','2018-09-28'),
( 'ドリフターズ',6,'平野耕太','2018-11-30'),
( '羅生門',1,'芥川龍之介','1915-11-01')
;
設定ファイル(application.properties)
接続先に?serverTimezone=JST
をつけないと上手くいかない。また、mybatis.configuration.map-underscore-to-camel-case=true
により、DBのカラム名がスネークケースであってもJava側ではキャメルケースとして認識してくれる。
### データベース接続設定
spring.datasource.url=jdbc:mysql://localhost:3306/library?serverTimezone=JST
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### スネークケースのDBカラム名をSpringのEntity側ではキャメルケースとして対応付けてくれる。
mybatis.configuration.map-underscore-to-camel-case=true
### 初期化を行うかの指定。
spring.datasource.initialization-mode=always
各種クラス
Formクラス
今回は画面からはidのみ受け取るのでフィールドは1個。HTML側のinputタグ内name属性とフィールド名は一致させておく。lombokの機能により、@Dataでセッターゲッターは追記不要。フィールドの型はプリミティブ型のint
ではなく、参照型(ラッパークラス)のInteger
にするのが良いらしい。ゼロとnullで区別がつくため。
package com.example.demo.form;
import lombok.Data;
@Data
public class BookForm {
private Integer id;
}
Entityクラス
データベースから取得したデータをいったん格納するオブジェクト。Formクラスにも書いたが、型は参照型が良い。
package com.example.demo.entity;
import java.time.LocalDate;
import lombok.Data;
@Data
public class Book {
private Integer id;
private String bookName;
private Integer volumeNum;
private String authorName;
private LocalDate publishedDate;
}
Daoインタフェースとマッピングファイル
MyBatisの場合は、インタフェースをつくり@MapperアノテーションをつけることでRepositoryクラスになる。1件検索のメソッドでは、単純に数値だけ渡すよりも、Entityクラス(Bookクラス)を介した方が、マッピングファイルの中で、Entityクラスの各フィールドを柔軟に参照できる。
package com.example.demo.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.demo.entity.Book;
@Mapper
public interface BookDao {
//1件検索
Book findById(Book book);
//全件取得
List<Book> findAll();
}
【XMLファイルについて】
- ファイル名は対応するDaoと同じにする。
- 配置場所はresorces配下のDaoインタフェースと同じ階層にする。
-
namespace属性
にはDaoインタフェースの完全修飾クラス名を書く。 -
select要素
にSELECT文を書く。-
id属性
にはDaoインタフェースの対応するメソッド名を書く。 -
resultType属性
には検索結果をマッピングするクラス名を書く。今回はBookクラス。 -
parameterType属性
は今回省略。メソッドの引数の型を書くらしい。省略すると、自動で実際の引数の型が判定される。
-
- 下記
findById
では、WHERE句でEntityクラス(Bookクラス)のidフィールドを参照している。
<?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.BookDao">
<select id="findById" resultType="com.example.demo.entity.Book">
SELECT
id,
book_name,
volume_num,
author_name,
published_date
FROM
booktable
WHERE
id = #{id}
</select>
<select id="findAll" resultType="com.example.demo.entity.Book">
SELECT
id,
book_name,
volume_num,
author_name,
published_date
FROM
booktable
</select>
</mapper>
Serviceクラス
Daoクラスでも書いたが、1件検索メソッドではEntityクラスのidフィールドに値をセットしている。全件取得はEntityクラスを要素にもつリストを返す。
package com.example.demo.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.dao.BookDao;
import com.example.demo.entity.Book;
@Service
public class BookService {
@Autowired
BookDao bookDao;
//1件検索
public Book findById(Integer id) {
Book book = new Book();
book.setId(id);
return this.bookDao.findById(book);
}
//全件取得
public List<Book> getBookList(){
return this.bookDao.findAll();
}
}
Controllerクラス
下記では@GetMappingや@PostMappingを使いわけてません。
package com.example.demo.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.entity.Book;
import com.example.demo.form.BookForm;
import com.example.demo.service.BookService;
@Controller
@RequestMapping("/book")
public class BookController {
@Autowired
BookService bookService;
@RequestMapping("/search")
public String index(BookForm bookForm, String showList, Model model) {
//タイトル
model.addAttribute("title", "本屋さん");
//bookform(formクラス)がnullじゃなかったら1件検索
if(bookForm.getId() != null) {
Book book = bookService.findById(bookForm.getId());
model.addAttribute("book", book);
}
//一覧表示ボタンが押されると本一覧をmodelに登録。
if(showList != null) {
List<Book> bookList = bookService.getBookList();
model.addAttribute("bookList", bookList);
}
return "index";
}
}
画面
th:if
でControllerのModelに登録したオブジェクトがnullかどうかで表示する内容が変わります。
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${title}">title</title>
<link href="/css/style.css" rel="stylesheet">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p>本屋さん</p>
<form action="/book/search" method="post">
<label>ID:<input class="input" type="text" name="id"></label><br>
<div><input class="search" type="submit" value="検索"/></div>
</form>
<form action="/book/search" method="post">
<div><input class="list" type="submit" name="showList" value="一覧表示"/></div>
</form>
<div th:if="${book} !=null" th:object="${book}">
<table>
<tr>
<th>ID</th>
<th>書籍名</th>
<th>巻</th>
<th>著者名</th>
<th>刊行日</th>
</tr>
<tr>
<td th:text="*{id}">id</td>
<td th:text="*{bookName}">書籍名</td>
<td th:text="*{volumeNum}">巻</td>
<td th:text="*{authorName}">著者名</td>
<td th:text="*{publishedDate}">刊行日</td>
</tr>
</table>
</div>
<div th:if="${bookList} !=null">
<table>
<tr>
<th>ID</th>
<th>書籍名</th>
<th>巻</th>
<th>著者名</th>
<th>刊行日</th>
</tr>
<tr th:each="book:${bookList}" th:object="${book}">
<td th:text="*{id}">id</td>
<td th:text="*{bookName}">書籍名</td>
<td th:text="*{volumeNum}">巻</td>
<td th:text="*{authorName}">著者名</td>
<td th:text="*{publishedDate}">刊行日</td>
</tr>
</table>
</div>
</body>
</html>
CSSよく分かりません。タグを[type=" "]で区別できたり、table thとスペース区切りで子要素を指定できたりする、ということはわかりました。。。
@charset "UTF-8";
input[type="text"]{
width:70%;
border-radius: 5px;
padding: 10px;
}
input[type="submit"].search{
border-radius: 5px;
padding: 5px;
margin-top: 10px;
background-color:#99CCCC;
}
input[type="submit"].list{
border-radius: 5px;
padding: 5px;
margin-top: 10px;
background-color: #008BBB;
color:#FFFFFF
}
table{
width: 100%;
margin-top: 10px;
border-collapse: collapse;
}
table th, table td {
border: 1px solid #ddd;
padding: 6px;
}
table th {
background-color: #6699FF;
}