Springbootの勉強のため、Dockerで作成したPostgre環境に接続してアプリケーションを作成します。
環境:Windows10
sts-4.10.0.RELEASE
Docker version 20.10.6
docker-compose version 1.29.1
→この記事のGitHubリポジトリ
https://github.com/jirentaicho/springbootsample
ブランチ:qiita
※docker-compose.ymlなどはdevフォルダに格納しております。
最終的なフォルダ構成
フォルダ構成をみる
C:.
│ .classpath
│ .gitignore
│ .project
│ HELP.md
│ mvnw
│ mvnw.cmd
│ pom.xml
│ readme.md
│
├─.mvn
│ └─wrapper
│ maven-wrapper.jar
│ maven-wrapper.properties
│ MavenWrapperDownloader.java
│
├─.settings
│ org.eclipse.core.resources.prefs
│ org.eclipse.jdt.apt.core.prefs
│ org.eclipse.jdt.core.prefs
│ org.eclipse.m2e.core.prefs
│ org.eclipse.wst.common.component
│ org.eclipse.wst.common.project.facet.core.xml
│ org.eclipse.wst.validation.prefs
│ org.springframework.ide.eclipse.prefs
│
├─dev
│ │ docker-compose.yml
│ │
│ └─postgres
│ └─setup
│ 1-ddl.sql
│ 2-dml.sql
│
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─volkruss
│ │ │ │ AnicomApplication.java
│ │ │ │ ServletInitializer.java
│ │ │ │
│ │ │ ├─application
│ │ │ │ ├─controller
│ │ │ │ │ InitializeController.java
│ │ │ │ │ RegisterController.java
│ │ │ │ │
│ │ │ │ └─request
│ │ │ │ AnimationRequest.java
│ │ │ │
│ │ │ ├─domain
│ │ │ │ ├─entity
│ │ │ │ │ AnimationEntity.java
│ │ │ │ │ AnimationRepository.java
│ │ │ │ │
│ │ │ │ ├─mapper
│ │ │ │ │ │ AnimationMapper.java
│ │ │ │ │ │
│ │ │ │ │ └─impl
│ │ │ │ │ AnimationMapperImpl.java
│ │ │ │ │
│ │ │ │ ├─model
│ │ │ │ │ Animation.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ AnimationService.java
│ │ │ │ │
│ │ │ │ └─Impl
│ │ │ │ AnimationServiceImpl.java
│ │ │ │
│ │ │ └─infrastructure
│ │ │ └─repository
│ │ │ AnimationJpaRepository.java
│ │ │ AnimationRepositoryImpl.java
│ │ │
│ │ ├─resources
│ │ │ │ application.properties
│ │ │ │
│ │ │ ├─static
│ │ │ └─templates
│ │ │ index.html
│ │ │ register.html
│ │ │
│ │ └─webapp
│ └─test
│ └─java
│ └─com
│ └─volkruss
│ AnicomApplicationTests.java
│
└─target
├─classes
│ │ application.properties
│ │
│ ├─com
│ │ └─volkruss
│ │ │ AnicomApplication.class
│ │ │ ServletInitializer.class
│ │ │
│ │ ├─application
│ │ │ ├─controller
│ │ │ │ InitializeController.class
│ │ │ │ RegisterController.class
│ │ │ │
│ │ │ └─request
│ │ │ AnimationRequest.class
│ │ │
│ │ ├─domain
│ │ │ ├─entity
│ │ │ │ AnimationEntity.class
│ │ │ │ AnimationRepository.class
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ │ AnimationMapper.class
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ AnimationMapperImpl.class
│ │ │ │
│ │ │ ├─model
│ │ │ │ Animation.class
│ │ │ │
│ │ │ └─service
│ │ │ │ AnimationService.class
│ │ │ │
│ │ │ └─Impl
│ │ │ AnimationServiceImpl.class
│ │ │
│ │ └─infrastructure
│ │ └─repository
│ │ AnimationJpaRepository.class
│ │ AnimationRepositoryImpl.class
│ │
│ └─templates
│ index.html
│ register.html
│
├─generated-sources
│ └─annotations
├─generated-test-sources
│ └─test-annotations
├─m2e-wtp
│ └─web-resources
│ └─META-INF
│ │ MANIFEST.MF
│ │
│ └─maven
│ └─com.volkruss
│ └─anicom
│ pom.properties
│ pom.xml
│
└─test-classes
└─com
└─volkruss
AnicomApplicationTests.class
Postgre環境の準備
Dockerを使ってpostgre環境を準備します。以前仕事では共通のDBサーバーがあって、スキーマを作るなどしてみんなで同じものを共有していました。
しかし、ある時から個人のローカルにDockerでDB環境作るようになってから、お構いなしにDBをぶっ壊したりしていました。
というわけでDockerでPostgre環境を用意します。
version: '3'
services:
postgre:
image: postgres:latest
ports:
- 5432:5432
container_name: my_postgre
volumes:
- ./postgres/data:/var/lib/postgresql/data
# 初期化用のシェルを格納しておきます。
- ./postgres/setup:/docker-entrypoint-initdb.d
environment:
POSTGRES_USER: misaka
POSTGRES_PASSWORD: mikoto
最初にsqlを実行するdocker-entrypoint-initdb.dに対して、postgres/setupフォルダをマウントしています。ここにsqlファイルを置くと最初に実行されます。
先頭に番号を振って実行順番を制御しています。
もし、Postgre環境を作り直したい時は以下のフォルダを削除します。
./postgres/data
(権限で消せないとか出たらpowershellで消してあげます rm -r data)
以下のコマンドでコンテナを立ち上げます
docker-compose up -d
STSでプロジェクト作成
以下のようにプロジェクトを作成しました。
データベースの接続設定
以下のファイルを修正して、コンテナのPostgreデータベースに接続するようにします。
anicom\src\main\resources\application.properties
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/misaka
spring.datasource.username=misaka
spring.datasource.password=mikoto
Viewを表示する
今、プロジェクトを実行すると、リクエストに対して何の設定も行っていないため、以下のようなページが表示されます。
まずは/にアクセスしたときにページを表示させるようにしていきます。
controllerの作成を行います。
controllerクラスには@Controllerアノテーションを付けます。このアノテーションをつけることで、HTMLなどのViewを返すことができます。
今回は/にアクセスしたときに、index.htmlを返すようにしています。
package com.volkruss.application.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class InitializeController {
@GetMapping("/")
public String getIndex() {
return "index";
}
}
index.htmlを作成します。
anicom\src\main\resources\templates\index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>アニメコム</title>
</head>
<body>
<p>お気に入りのアニメとキャラクターを登録しよう</p>
</body>
</html>
これで実行すると以下のような画面が表示されています。
リクエストの情報をコントローラーが受け取ってViewを返すということができるようになりました。
データベースのレコードを表示する
次はデータベースからレコードを取得して、表示させます。
EntityととRepositoryとModelを作成します。
設定の見直し
Entityは実際のテーブルの構造と紐づいていますので、そのままテーブルの構造を反映させます。
アクセサについては簡略化させたいので、lombokを利用します。pom.xmlに以下を追加します。
あと、プロジェクト作成時にJPAを追加してなかったので、jpaの依存も追加します。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 追加 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<!-- もう一個追加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
SpringToolSuite4.iniに以下を追記します。
-javaagent:lombokのパス\lombok.jar
-vmarges -javaagent:lombok.jar
Entityの作成
ようやくEntityクラスを作成できます。テーブルの構造と紐づけたいので以下のようにします。
package com.volkruss.domain.entity;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@Entity
@Table(name = "m_animation")
public class AnimationEntity{
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "m_animation_id_seq")
@SequenceGenerator(name = "m_animation_id_seq",sequenceName = "m_animation_id_seq",initialValue = 1,allocationSize = 1)
private int id;
@Column(name = "title")
private String title;
@Column(name = "broadcast_start")
private Date broadcast_start;
@Column(name = "broadcast_end")
private Date broadcast_end;
@Column(name = "created_at")
private Date created_at;
@Column(name = "updated_at")
private Date updated_at;
}
Modelの作成
続いてModelを作成します。現在放送中かどうかを返せるようなメソッドを定義しました。
package com.volkruss.domain.model;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class Animation {
private int id;
private String title;
private Date broadcast_start;
private Date broadcast_end;
/**
* <P>
* 現在放送中かどうかを返します。
* </p>
*
* @return 放送期間が現在時刻の範囲ならばTrue / そうでない場合はfalse
*/
public boolean isBroadNow() {
// 日付の範囲内の場合True
Date today = new Date();
return today.after(broadcast_start) && today.before(broadcast_end);
}
}
Repositoryの作成
AnimationEntityを取得するJpaRepositoryを継承したAnimationJpaRepositoryインターフェースを作成します。必要最低限のデータアクセスに関するメソッドが提供されているので、こちらを利用します。
そして、AnimationJpaRepositoryを利用するAnimationRepositoryを作成します。
AnimationJpaRepositoryから取得したEntityをModelに変換して返却します。
この時に変換するのにAnimationMapperクラスを用意します。
AnimationJpaRepositoryの作成
SQL文を書かなくてもEntityを利用することでデータアクセスができるようになります。
そのために、JpaRepositoryを継承したインターフェースを作成します。
package com.volkruss.infrastructure.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.volkruss.domain.entity.AnimationEntity;
public interface AnimationJpaRepository extends JpaRepository<AnimationEntity,String>{
}
AnimationRepositoryの作成
AnimationJpaRepositoryとAnimationMapperを利用して、Animationモデルを返すクラスです。
インターフェースは適宜作成しておきます。
package com.volkruss.infrastructure.repository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;
@Repository
public class AnimationRepositoryImpl implements AnimationRepository {
@Autowired
private AnimationJpaRepository animationJpaRepository;
@Autowired
private AnimationMapper animationMapper;
@Override
public List<Animation> getAll() {
List<AnimationEntity> lists = animationJpaRepository.findAll();
return lists.stream().map(animationMapper::toAnimation).collect(Collectors.toList());
}
}
AnimationMapperの作成
EntityをModelに変換するクラスを作成します。
こちらもインターフェースを適宜作成しておきます。
package com.volkruss.domain.mapper.impl;
import org.springframework.stereotype.Component;
import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;
@Component
public class AnimationMapperImpl implements AnimationMapper{
/**
* {@inheritDoc}
*/
@Override
public Animation toAnimation(AnimationEntity entity) {
Animation animation = new Animation();
animation.setTitle(entity.getTitle());
animation.setBroadcast_start(entity.getBroadcast_start());
animation.setBroadcast_end(entity.getBroadcast_end());
return animation;
}
}
Serviceの作成
今回は取得するだけなので、repositoryからモデルを取得します。
package com.volkruss.domain.service.Impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;
@Service
public class AnimationServiceImpl implements AnimationService{
@Autowired
private AnimationRepository animationRepository;
@Override
public List<Animation> getAnimationList() {
return animationRepository.getAll();
}
}
コントローラーの修正
controllerからserviceを利用してモデルを取得して、その結果をViewに反映させるように修正します。
package com.volkruss.application.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.GetMapping;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;
@Controller
public class InitializeController {
@Autowired
private AnimationService animationService;
@GetMapping("/")
public String getIndex(Model model) {
List<Animation> animations = animationService.getAnimationList();
// modelに値を設定してViewで利用できるようなる
model.addAttribute("animations",animations);
return "index";
}
}
Viewの修正
Animationモデルを受けっているので、Viewで表示していきます。
モデルのisBroadNowメソッドを利用して、放送中かどうかを判定しています。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>アニメコム</title>
</head>
<body>
<p>お気に入りのアニメとキャラクターを登録しよう</p>
<table>
<thead>
<tr>
<th colspan="2">アニメ情報</th>
</tr>
</thead>
<tbody th:each="animation : ${animations}">
<tr>
<td:block th:if="${animation.isBroadNow()}">
<td th:text="放送中">放送中</td>
</td:block>
<td:block th:if="${!animation.isBroadNow()}">
<td th:text="放送していません">放送していません</td>
</td:block>
<td th:text="${animation.title}">タイトル</td>
<td th:text="${#strings.substring(animation.broadcast_start,0,10)}">放送開始日</td>
</tr>
</tbody>
</table>
</body>
</html>
以下のように表示されていれば、データベースからレコードを取得して、Viewに表示するというところまでできました。
見た目も素晴らしいですね(笑)
登録処理を作成する
サクッと登録処理を作成します。
コントローラーの修正
コントローラーに登録時ページへのマッピング情報を追加します。
モデルにリクエストクラスのインスタンスを設定しています。
このリクエストクラスは、HTMLのフォームの内容と紐づいております。
package com.volkruss.application.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.GetMapping;
import com.volkruss.application.request.AnimationRequest;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;
@Controller
public class InitializeController {
@Autowired
private AnimationService animationService;
@GetMapping("/")
public String getIndex(Model model) {
List<Animation> animations = animationService.getAnimationList();
// modelに値を設定してViewで利用できるようなる
model.addAttribute("animations",animations);
return "index";
}
//追加
@GetMapping("/register")
public String getRegister(Model model) {
model.addAttribute("animationReqeust", new AnimationRequest());
return "register";
}
}
リクエストクラスの作成
formに紐づくリクエストクラスを作成します。
package com.volkruss.application.request;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class AnimationRequest {
private String title;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date broadcast_start;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date broadcast_end;
}
Viewの作成
register.htmlを追加します。ここでformを作成してPOSTにてデータを送信できるようにします。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>アニメコム</title>
</head>
<body>
<form th:action="@{register}" th:object="${animationReqeust}" method="POST" >
<div>
<label>タイトル</label>
<input type="text" th:field="*{title}" required>
</div>
<div>
<label>放送開始</label>
<input type="date" th:field="*{broadcast_start}" required>
</div>
<div>
<label>放送終了</label>
<input type="date" th:field="*{broadcast_end}" required>
</div>
<button type="submit">登録する</button>
</form>
</body>
</html>
ついでにこのViewへのリンクをindex.htmlに作成します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>アニメコム</title>
</head>
<body>
<p>お気に入りのアニメとキャラクターを登録しよう</p>
<table>
<thead>
<tr>
<th colspan="2">アニメ情報</th>
</tr>
</thead>
<tbody th:each="animation : ${animations}">
<tr>
<td:block th:if="${animation.isBroadNow()}">
<td th:text="放送中">放送中</td>
</td:block>
<td:block th:if="${!animation.isBroadNow()}">
<td th:text="放送していません">放送していません</td>
</td:block>
<td th:text="${animation.title}">タイトル</td>
<td th:text="${#strings.substring(animation.broadcast_start,0,10)}">放送開始日</td>
</tr>
</tbody>
</table>
<!-- 追加 -->
<a href="/register" th:href="@{/register}">新規登録する</a>
</body>
</html>
コントローラーの作成
次にformの値を受け取る必要があるのでコントローラーを新しく作成します。
今回は妥当性確認処理などを省いて、フォームからの値を登録してトップページにリダイレクトさせています。
package com.volkruss.application.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import com.volkruss.application.request.AnimationRequest;
import com.volkruss.domain.service.AnimationService;
@Controller
public class RegisterController {
@Autowired
private AnimationService animationService;
@PostMapping("/register")
public String register(AnimationRequest request,Model model) {
animationService.insert(request);
return "redirect:/";
}
}
登録ロジックの作成
insert処理を作成します。
まずはserviceクラスを修正します。
モデルを作成してrepositoryに渡します。
package com.volkruss.domain.service.Impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.volkruss.application.request.AnimationRequest;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;
@Service
public class AnimationServiceImpl implements AnimationService{
@Autowired
private AnimationRepository animationRepository;
@Override
public List<Animation> getAnimationList() {
return animationRepository.getAll();
}
// 追加
@Override
public void insert(AnimationRequest request) {
Animation animation = create(request);
animationRepository.insert(animation);
}
// 追加
/**
* <P>
* リクエストからモデルを作成します。
* </P>
* @param request
* @return
*/
private Animation create(AnimationRequest request) {
Animation animation = new Animation();
animation.setTitle(request.getTitle());
animation.setBroadcast_start(request.getBroadcast_start());
animation.setBroadcast_end(request.getBroadcast_end());
return animation;
}
}
repositoryの修正
Mapperを利用してmodelをentityに変換したら、JPAのsaveメソッドを使ってデータベースにレコードを登録します。
package com.volkruss.infrastructure.repository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;
@Repository
public class AnimationRepositoryImpl implements AnimationRepository {
@Autowired
private AnimationJpaRepository animationJpaRepository;
@Autowired
private AnimationMapper animationMapper;
/**
* {@inheritDoc}
*/
@Override
public List<Animation> getAll() {
List<AnimationEntity> lists = animationJpaRepository.findAll();
return lists.stream().map(animationMapper::toAnimation).collect(Collectors.toList());
}
// 追加
/**
* {@inheritDoc}
*/
@Override
public void insert(Animation animation) {
AnimationEntity entity = animationMapper.toAnimationEntity(animation);
animationJpaRepository.save(entity);
}
}
Mapperの修正
modelをentityに変換します。
idはシーケンスを利用しているので勝手に連番が付与されます。
package com.volkruss.domain.mapper.impl;
import java.util.Objects;
import org.springframework.stereotype.Component;
import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;
@Component
public class AnimationMapperImpl implements AnimationMapper{
/**
* {@inheritDoc}
*/
@Override
public Animation toAnimation(AnimationEntity entity) {
Animation animation = new Animation();
animation.setTitle(entity.getTitle());
animation.setBroadcast_start(entity.getBroadcast_start());
animation.setBroadcast_end(entity.getBroadcast_end());
return animation;
}
// 追加
/**
* {@inheritDoc}
*/
@Override
public AnimationEntity toAnimationEntity(Animation animation) {
AnimationEntity entity = new AnimationEntity();
if(Objects.nonNull(animation.getId())) {
entity.setId(animation.getId());
}
entity.setTitle(animation.getTitle());
entity.setBroadcast_start(animation.getBroadcast_start());
entity.setBroadcast_end(animation.getBroadcast_end());
return entity;
}
}
確認
登録ページ
登録後
### 終わりに
取得と登録のみですができました。
本当なら妥当性確認などの処理も入ってきます。