[AWS SES] Simple Email Serviceで申請が落ちた場合の対処法

アプリケーション開発において、メール送信機能は必須であり、それが出来ないとなると、根幹を大きく揺るぎかねない。

メールリレーとして比較すると、mailgun, ses, sendgrid, ベアメールなど色々ある。
が、AWSで開発するならば、親和性が良いので、SESを使いたい。

Route53で登録したドメインからメール送信できるようSESで設定していくのだが、
初期設定では、sandboxが設定されており、送信先が制限されている。

その為、送信制限解除の申請を行う。
こちらのブログが参考になる。
https://www.grandream.jp/blog/aws-ses/

で、落ちることはないだろうとたかを括っていたら、、、 6時間後くらいに、、、、

Thank you for submitting your request to increase your sending limits for your Amazon SES account in the Asia Pacific (Tokyo) region. We are unable to grant your request at this time.

An account that is related to your current account is paused in the US East (N. Virginia) region.

嘘やろ。。。マジかよ。。。
頭真っ白になった。。。

取り敢えず以下の解答を書いて、一先ず寝た。

If you believe we arrived at this decision in error, please respond to this case and provide the following information:

-- What is the nature of your business, and how do you plan to use Amazon SES to meet the needs of your business?

-- How do you collect the email addresses that are on your mailing list?

-- How do your subscribe and unsubscribe processes work? Include links to your opt-in and opt-out pages.

-- How do you handle bounces and complaints?

-- What type of email (for example, transactional notifications, marketing content, or system notifications) do you plan to send with Amazon SES?

-- What is the URL of your website?

-- Is there any other information that would help us better understand your use case?

SESは、申請が通らないことがあるから、代替手段を考えておいた方が良い。
ちなみに私はMailgunでもテストしていたので、急遽、Mailgunで実装するテストを開始した。

で、更に4時間後くらいに、、、

平素は Amazon Web Services をご利用いただき、誠にありがとうございます。

このたびは、送信制限の引き上げ申請をご送信いただき、ありがとうございます。新たな送信クォータは、1 日あたり 50,000 メッセージとなります。新たな最大送信レートは、毎秒 14 メッセージです。また、お客様のアカウントを Amazon SES サンドボックスから移動いたしました。

この引き上げは、アジアパシフィック (東京)リージョンにおいて、直ちに有効になります。Amazon SES コンソールの送信統計情報のページで、または GetSendQuota API を使って、お客様のアカウントの現在の送信レートと送信クォータを確認することができます。
// 省略

オイオイオイ、通るんかよ。

### 教訓(重要)
– SESは申請が通らないことがあるので、SESが駄目だった時のために、Mailgunなどで準備をしておいた方が絶対に良い(精神的にも)
– SESは英語で申請すると、中国人などが対応するので、日本語で申請した方が良い
– 一度申請が落ちても、きちんと再度回答すると、申請が通る

結果オーライだけど、全くひどい週末になったよ。

### AWSで売れ筋の本です

[Spring Boot][java] ログイン機能

Spring Securityを使用する
pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity4</artifactId>
		</dependency>

ユーザ取得処理の実装
ReservationUserDetails.java

package mrs.domain.service.user;

import java.util.Collection;
import mrs.domain.model.User;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.secuirty.core.authority.AuthorityUtils;
import org.springframework.secuirty.core.userdetails.UserDetails;

public class ReservationUserDetails implements UserDetails {
	private final User user;
	
	public ReservationUserDetails(User user) {
		this.user = user;
	}
	
	public User getUser() {
		return user;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities(){
		return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName().name());
	}
	
	@Override
	public String getPassword() {
		return this.user.getPassword();
	}
	
	@Override
	public String getUsername() {
		return this.user.getUserId();
	}
	
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	
	@Override
	public boolean isCredentialsExpired() {
		return true;
	}
	
	@Override
	public boolean isEnabled() {
		return true;
	}
}

ん、認証って、一つ一つ書くんか。

UserRepository.java

package mrs.domain.repository.user;

import org.springframework.data.jpa.repository.JpaRepository;

import mrs.domain.model.User;

public interface UserRepository extends JpaRepository<User, String>{
	
}

ReservationUserDetailsService.java

package mrs.domain.service.user;

import mrs.domain.model.User;
import mrs.domain.repository.user.UserRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class ReservationUserDetailsService implements UserDetailsService {
	@Autowired
	UserRepository userRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username)
		throws UsernameNotFoundException {
		User user = userRepository.findOne(username);
		if(user == null) {
			throw new UsernameNotFoundException(username + " is not found.");
		}
		return new ReservationUserDetails(user);
	}
}

LoginController.java

package mrs.app.login;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
	@RequestMapping("loginForm")
	String loginForm() {
		return "login/loginForm";
	}
}

プロジェクトルートで ./mvnw package で.jarファイルを作成する
うむ、何がわかってないかがわかってない状態だ。

[設計] マルチテナントアーキテクチャー

AWSはvpcが5つまでしか使わないので、複数のクライアントと契約する場合、どうするのかずっと気になっていた。あと、freee, money forwardみたいに、沢山のユーザがいる場合はどう設計しているのか?

マルチテナントアーキテクチャーで設計するらしい。
1. クライアントごとにデータベースを分ける
2. クライアントごとにidを振って、同じデータベースで管理する
3. ハードウェアのみ共有する

軽いアプリケーションなら、2の同じデータベースで良さそうだが、大きくスケールすることを考えたら、2のクライアントごとにデータベースを別ける方法か。。

うーむ、ちょっと時間かけて勉強したいテーマやないか。

[Spring Boot][java] app開発4

ReservationsController.java

@RequestMapping(method = RequestMethod.POST)
	String reserve(@Validated ReservationForm form, BindingResult bindingResult,
			@DateTimeFormat(iso=DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
			@PathVariable("roomId") Integer roomId, Model model) {
		if(bindingResult.hasErrors()) {
			return reserveForm(date, roomId, model);
		}
		
		ReservableRoom reservableRoom = new ReservableRoom(
				new ReservableRoomId(roomId, date));
		Reservation reservation = new Reservation();
		reservation.setStarttime(form.getStartTime());
		reservation.setEndTime(form.getEndTime());
		reservation.setReservableRoom(reservableRoom);
		reservation.setUser(dummyUser());
		
		try {
			reservationService.reserve(reservation);
		}
		catch (UnavailableReservationException | AlreadyReservedException e) {
			model.addAttribute("error", e.getMessage());
			return reserveForm(date, roomId, model);
		}
		return "redirect:/reservations/{date}/{roomId}";
	}
	
	@RequestMapping(method = RequestMethod.POST, params = "cancel")
	String cancel(@RequestParam("reservationId") Integer reservationId,
			@PathVariable("roomId") Integer roomId,
			@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
			Model model) {
		User user = dummyUser();
		try {
			reservationService.cancel(reservationId, user);
		}
		catch (IllegalStateException e) {
			model.addAttribute("error", e.getMessage());
			return reserveForm(date, roomId, model);
		}
		return "redirect:/reservations/{date}/{roomId}";
	}

ThirtyMinutesUnit.java

package mrs.app.reservation;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.*;

import javax.validation.*;

@Documented
@Constraint(validatedBy = {ThirtyMinutesUnitValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RINTIME)
public @interface ThirtyMinutesUnit {
	String message() default "{mrs.app.reservation.ThirtyMinutesUnit.message}";
	
	Class<?>[]groups() default {};
	
	Class<? extends Playload>[]playload() default {};
	
	@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		ThirtyMinutesUnit[]value();
	}
}

ThirtyMinutesUnitValidator.java

package mrs.app.reservation;

import java.time.LocalTime;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class ThirtyMinutesUnitValidator
	implements ConstraintValidator<ThirtyMinutesUnit, LocalTime>{
	@Override
	public void initialize(ThirtyMinutesUnit constraintAnnotation) {
		
	}
	
	@Override
	public boolean isValid(LocalTime value, ConstraintValidatorContext context) {
		if(value == null) {
			return true;
		}
		return value.getMinute() % 30 == 0;
	}
}

EndTimeMuustBeAfterStartTime.java

package mrs.app.reservation;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.*;

import javax.validation.*;

@Documented
@Constraint(validatedBy = {EndTimeMustBeAfterStartTimeValidator.class})
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface EntTimeMustBeAfterStartTime{
	String message() default "{mrs.app.reservation.EndTimeMustBeAfterStartTime.message}";
	
	Class<?>[]groups() default{};
	
	Class<? extends Playload>[]payload() default {};
	
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		EndTimeMustBeAfterStartTime[]value();
	}
}

EndTimeMustBeAfterStartTimeValidator.java

package mrs.app.reservation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EndTimeMustBeAfterStartTimeValidator
	implements ConstraintValidator<EndTimeMustBeAfterStartTime, ReservationForm>{
	private String message;
	
	@Override
	public void initialize(EndTimeMustBeAfterStartTime constraintAnnotation) {
		message = constraintAnnotation.message();
	}
	
	@Override
	public boolean isValid(ReservationForm value, ConstraintValidatorContext context) {
		if(value.getStarttime() == null || value.getEndTime() == null) {
			return true;
		}
		boolean isEndTimeMustBeAfterStartTime = value.getEndTime().isAfter(value.getStartTime());
		if(!isEndTimeMustBeAfterStartTime) {
			context.disableDefaultConstraintViolation();
			context.buildConstraintViolationWithTemplate(message).addPropertyNode("endTime").addConstraintViolation();
		}
		return isEndTimeMustBeAfterStartTime;
	}
}

うーむ、ちょっと混乱してきたな。

[Spring Boot][java] app開発3

SpringBootのリポジトリとは?
-> DBへのアクセスを自動で作成する便利インターフェース
-> DBへのCRUDを自動で実装してくれる
なるほど

Listとは?
-> Javaのリスト(List)とは、重複した要素を含むことができる順序の付けられたコレクション
-> 配列とは異なる

ReservationRepository.java

package mrs.domain.repository.reservation;

import java.util.List;

import mrs.domain.model.ReservableRoomId;
import mrs.domain.model.Reservation;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ReservationRepository extends JpaRepository<Reservation, Integer>{
	List<Reservation> findByReservableRoom_ReservableRoomIdOrderByStartTimeAsc(
			ReservableRoomId reservableRoomId);
}

MeetingRoomRepository.java

package mrs.domain.repository.room;

import org.springframework.data.jpa.repository.JpaRepository;

import mrs.domain.model.MeetingRoom;

public interface MeetingRoomRepository extends JpaRepository<MeetingRoom, Integer>{
	
}

Serviceとは?
-> ビジネスロジックを実装するクラス
-> Controllerはリクエストの窓口になるクラス
ん、ビジネスロジックはcontrollerではなく、serviceで書くのか。ちょっと混乱するな。

ReservationService.java

package mrs.domain.service.reservation;

import java.util.List;

import mrs.domain.model.*;
import mrs.domain.repository.reservation.ReservationRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ReservationService {
	@Autowired
	ReservationRepository reservationRepository;
	
	public List<Reservation> findReservations(ReservableRoomId reservableRoomId){
		return reservationRepository.findByReservableRoom_ReservableRoomIdOrderByStartTimeAsc(
				reservableRoomId);
	}
}

Serviceの中のpublic List<*>がJavaの構文でないような印象だが、どういう事なんだ?

package mrs.domain.service.reservation;

import java.util.List;

import mrs.domain.model.*;
import mrs.domain.repository.reservation.ReservationRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ReservationService {
	@Autowired
	ReservationRepository reservationRepository;
	@Autowired
	ReservableRoomRepository reservableRoomRepository;
	
	public Reservation reserve(Reservation reservation) {
		ReservableroomId reservableRoomId = reservation.getReservableRoom().getReservableRoomId();
		ReservableRoom reservable = reservableRoomRepository.findOne(reservableRoomId);
		if(reservable == null) {
			
		}
		boolean overlap = reservationRepository.findByReservableRoom_ReservableRoomIdOrderByStartTimeAsc(reservableRoomId)
				.stream()
				.anyMatch(x -> x.overlap(reservation));
		if(overlap) {
			
		}
		reservationRepository.save(reservation);
		return reservation;
	}

ReservationsController.java

package mrs.app.reservation;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import mrs.domain.model.*;
import mrs.domain.service.reservation.*;
import mrs.domain.service.room.RoomService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("reservations/{date}/{roomId}")
public class ReservationsController {
	@Autowired
	RoomService roomService;
	@Autowired
	ReservationService reservationService;
	
	@ModelAttribute
	ReservationForm setUpForm() {
		RservationForm form = new ReservationForm();
		form.setStartTime(LocalTime.of(9, 0));
		form.setEndTime(LocalTime.of(10, 0));
		return form;
	}
	
	@RequestMapping(method = RequestMethod.GET)
	String reserveForm(@DateTimeFormat(iso=DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
			@PathVariable("roomId") Integer roomId, Model model) {
		ReservableRoomId reservableRoomId = new ReservableRoomId(roomId, date);
		List<Reservation> reservations = reservationService.findReservations(reservableRoomId);
		
		List<LocalTime> timeList =
				Stream.iterate(LocalTime.of(0, 0), t -> t.plusMinutes(30))
				.limit(24 * 2)
				.collect(Collectors.toList());
		
		model.addAttribute("room", roomService.findMeetingRoom(roomId));
		model.addAttribute("reservations", reservations);
		model.addAttribute("timeList", timeList);
		model.addAttribute("user", dummyUser());
		return "reservation/reserveForm";
	}
	
	private User dummyUser() {
		User user = new User();
		user.setUserId("taro-yamada");
		user.setFirstName("taro");
		user.setLastName("Yamada");
		user.setRoleName(RoleName.USER);
		return user;
	}
}
package mrs.app.reservation;

import java.io.Serializable;
import java.time.LocalTime;

import javax.validation.constraints.NotNull;

import org.springframework.format.annotation.DateTimeFormat;

public class ReservationForm implements Serializable {
	@NotNull(message = "必須です")
	@DateTimeFormat(pattern = "HH:mm")
	private LocalTime startTime;
	
	@NotNull(message = "必須です")
	@DateTimeFormat(pattern = "HH:mm")
	private Localtime endTime;
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title th:text="|${#temporals.format(date, 'yyyy/M/d')}の${room.roomName}|">2021/01/29の豊洲</title>
</head>
<body>

<div>
<a th:href="@{'/rooms/' + ${date}">会議室一覧へ</a>
</div>

<form th:object="${reservationForm}"
	th:action="@{'/reservations/' + ${date} + '/' + ${roomId}}" method="post">
	会議室: <span th:text="${room.roomName}">銀座</span>
	<br>
	予約者名: <span th:text="${user.lastName + ' ' + user.firstName}">山田太郎</span>
	// 省略

うーむ、全体的な流れを掴むには良いが、もう少し入門的なところからやりたい。。

[Spring Boot][java] app開発2

src/main/resources/schema.sql

DROP TABLE IF EXISTS meeting_room CASCADE;
DROP TABLE IF EXISTS reservable_room CASCADE;
DROP TABLE IF EXISTS reservation CASCADE;
DROP TABLE IF EXISTS usr CASCADE;

CREATE TABLE IF NOT EXIST meeting_room(
	room_id SERIAL NOT NULL,
	room_name VARCHAR(255) NOT NULL,
	PRIMARY KEY (room_id)	
);


CREATE TABLE IF NOT EXIST reservable_room(
	reserved_data DATE NOT NULL,
	room_id INT4 NOT NULL,
	PRIMARY KEY(reserved_date, room_id)
);

CREATE TABLE IF NOT EXIST meeting_room(
	reservation_id SERIAL NOT NULL,
	end_tim TIME NOT NULL,
	start_time TIME NOT NULL,
	reserved_date DATE NOT NULL,
	room_id INT4 NOT NULL,
	user_id VARCHAR(255) NOT NULL,
	PRIMARY KEY(reservation_id)
);

CREATE TABLE IF NOT EXIST meeting_room(
	user_id VARCHAR(255) NOT NULL,
	first_name VARCHAR(255) NOT NULL,
	last_name VARCHAR(255) NOT NULL,
	password VARCHAR(255) NOT NULL,
	role_name VARCHAR(255) NOT NULL,
	PRIMARY KEY (user_id)
);

repository class

package mrs.domain.repository.room;

import java.time.LocalDate;
import java.util.List;

import mrs.domain.model.ReservableRoom;
import mrs.domain.model.ReservableRoomId;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ReservableRoomRepository extends JpaRepository<ReservableRoom, ReservableRoomId>{
	List<ReservableRoom> findByReservableRoom_reservedDateOrderByReservableRoomId_roomIdAsc(LocalDate reservedDate);
}

-> importで先ほど作ったmodelを読み込んでますね。なるほど、少しわかってきた。

domain/service/RoomService.java

package mrs.domain.repository.room;

import java.time.LocalDate;
import java.util.List;

import mrs.domain.model.ReservableRoom;
import mrs.domain.repository.room.ReservableRoomRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class RoomService {
	
	@Autowired
	ReservableRoomRepository reservableRoomRepository;
	
	public List<ReservableRoom> findReservableRooms(LocalDate date){
		return reservableRoomRepository.findByReservableRoomId_reservedDateOrderByReservableRoomId_roomIdAsc(date);
	}
}

app/room/RoomsController.java

package mrs.app.room;

import java.time.LocalDate;
import java.util.List;

import mrs.domain.model.ReservableRoom;
import mrs.domain.service.room.RoomService;

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 org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("rooms")
public class RoomsController {
	@Autowired
	RoomService roomService;
	
	@RequestMapping(method = RequestMethod.GET)
	String listRooms(Model model)
		LocalDate today = LocalDate.now();
		List<ReservableRoom> rooms = roomService.findReservableRooms(today);
		model.addAttribute("date", today);
		model.addAttribute("rooms", rooms);
		return "room/listRooms";
}

-> ん? ControllerのRequestMappingでクエリ検索してる?

/main/resources/templates/room/listRooms.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title th:text="|${#temporals.format(date, 'yyyy/M/d')}の会議室|">2021/01/29の会議室</title>
</head>
<body>
<h3>会議室</h3>
<a th:href="@{'/rooms/' + ${date.minusDays(1)}}">&lt; 前日</a>
<span th:text="|${#temporals.format(date, 'yyyy/M/d')}の会議室|">2021/01/29の会議室</span>
<a th:href="@{'/rooms/' + ${date.plusDays(1)}}">翌日 &gt;</a>

<ul>
	<li th:each="room: ${rooms}">
		<a th:href="@{'/reservations/' + ${date}+ '/' + ${room.meetingRoom.roomId}}"
		 th:text="${room.meetingRoom.roomName}"></a>
	</li>
</ul>
</body>
</html>

-> thymeleafはthで変数入れてますね。th:eachはforeachっぽい。

RoomsController

	String listRooms(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date, Model model)

規約が多い印象だが、想像してたより複雑ではなさそう。。

[Spring Boot][java] app開発1

STS4 -> File -> Spring Starter Project


SQL: JPA, PostgreSQL
Template Engines: Thymeleaf
Web: Web

ドメイン層: Model, Repository, Service
アプリケーション層: Controller, View(HTML)

# postgresでデータベース作成
$ createdb test -O root

Spring Bootの設定はapplication.propertiesに集約されている
application.properties

spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=root
spring.datasource.password=hoge
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.format_sql=true
spring.datasource.sql-script-encoding=UTF-8
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# ライブラリ追加

		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-java8time</artifactId>
			<scope>2.1.0.RELEASE</scope>
		</dependency>

モデルの配下にエンティティを作成していく
User.java

package mrs.domain.model;

import java.io.Serializable;

import javax.persistence.*;

@Entity
@Table(name="usr")
public class User implements Serializable {
	@Id
	private String userId;
	private String password;
	private String firstName;
	private String lastName;
	
	@Enumerated(EnumType.STRING)
	private RoleName roleName;
}

RoleName.java

package mrs.domain.model;

public enum RoleName {
	ADMIN, USER
}

MeetingRoom.java

package mrs.domain.model;

import java.io.Serializable;

import javax.persistence.*;

@Entity
public class MeetingRoom implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer roomId;
	
	private String roomName;
}

ReservableRoom.java

package mrs.domain.model;

import java.io.Serializable;

import javax.persistence.*;

@Entity
public class ReservableRoom implements Serializable {
	@EmbeddedId
	private ReservableRoomId reservableRoomId;
	
	@ManyToOne
	@JoinColumn(name="room_id", insertable=false, updatable= false)
	@MapsId("roomId")
	private MeetingRoom meetingRoom;
	
	public ReservableRoom(ReservableRoomId reservableRoomId) {
		this.reservableRoomId = reservableRoomId;
	}
	public ReservableRoom() {
		
	}
}

ReservedRoomId.java

package mrs.domain.model;

import java.io.Serializable;
import java.time.LocalDate;

import javax.persistence.Embeddable;

@Embeddable
public class ReservableRoomId implements Serializable {
	
	private Integer roomId;
	private LocalDate reservedDate;
	
	public ReservableRoomId(Integer roomId, LocalDate reservedDate) {
		this.roomId = roomId;
		this.reservedDate = reservedDate;
	}
	
	public ReservableRoomId() {
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((reservedDate == null)? 0 : reservedDate.hashCode());
		result = prime * result + ((roomId == null) ? 0 : roomId.hashCode());
		return result;
	}
	
	@Override
	public boolean equals(Object obj) {
		if(this == obj) return true;
		if(obj == null) return false;
		
		if(getClass() != obj.getClass()) return false;
		ReservableRoomId other = (ReservableRoomId) obj;
		if(reservedDate == null) {
			if(other.reservedDate != null) return false;
		} else if (!reservedDate.equals(other.reservedDate))
			return false;
		if(roomId == null) {
			if(other.roomId != null) return false;
		} else if (!roomId.equals(other.roomId))
			return false;
		return false;
	}
}

Reservation.java

package mrs.domain.model;

import java.io.Serializable;
import time.LocalTime;

import javax.persistence.*;

@Entity
public class Reservation implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer reservationId;
	
	private LocalTime startTime;
	private LocalTime endTime;
	
	@ManyToOne;
	@JoinColumns({@JoinColumn(name = "reserved_date"),
		@JoinColumn(name = "room_id")})
	private RservableRoom reservableRoom;
	
	@ManyToOne
	@JoinColumn(name="user_id")
	private User user;
}

[postgres] macにインストール

Spring Boot

$ postgres –version
-bash: postgres: command not found
$ brew search postgresql
==> Formulae
postgresql postgresql@11 postgresql@9.4 postgresql@9.6
postgresql@10 postgresql@12 postgresql@9.5
==> Casks
navicat-for-postgresql

最新版をインストールします。
$ brew install postgresql
To have launchd start postgresql now and restart at login:
brew services start postgresql
Or, if you don’t want/need a background service you can just run:
pg_ctl -D /usr/local/var/postgres start

$ postgres –version
postgres (PostgreSQL) 13.1
$ pg_ctl -D /usr/local/var/postgres start

# ユーザ作成
$ createuser -P root
# データベース作成
$ createdb test -O root
$ psql -l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
———–+——-+———-+———+——-+——————-
postgres | mac | UTF8 | C | C |
template0 | mac | UTF8 | C | C | =c/mac +
| | | | | mac=CTc/mac
template1 | mac | UTF8 | C | C | =c/mac +
| | | | | mac=CTc/mac
test | root | UTF8 | C | C |
(4 rows)

-> “test”が作成された

### psqlログイン
$ psql -U root test
psql (13.1)
Type “help” for help.

test=>

なんか異常にシンプルやなー

[Spring Boot][java] データアクセス

pom.xml

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

jdbcTemplate使用
MessagesController.java

package com.example.demo;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("messages")
public class MessagesController {
	@Autowired
	JdbcTemplate jdbcTemplate;
	
	@RequestMapping(method = RequestMethod.GET)
	public List<Message> getMessages(){
		return jdbcTemplate.query("SELECT text FROM messages ORDER BY id", (rs, i)->{
			Message m = new Message();
			m.setText(rs.getString("text"));
			return m;
		});
	}
	
	@RequestMapping(method = RequestMethod.POST)
	public Message postMessage(@RequestBody Message message) {
		jbdcTemplate.update("INSERT INTO messages(text) VALUES (?)", message.getText());
		
	}
}

spring bootはクラスパス直下にschema.sqlが存在すると、起動時にそのsqlを実行する

CREATE TABLE messages (
	id INT PRIMARY KEY AUTO_INCREMENT,
	text VARCHAR(255)
);

### PostgreSQLを使う方法

		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>

application.properties

string.datasource.username=root
string.datasource.password=hoge
spring.datasource.url=jdbc:postgresql://localhost:5432/spring

mysqlを使用する場合は、schema.sqlを用意するのが定石か

[Spring Boot][java] RESTfulのサンプル作成

Message.java

import java.io.Serializable;

public class Message implements Serializable {
	private String text;
	
	public String getText() {
		return text;
	}
	
	public void setText(String text) {
		this.text = text;
	}
}

MessagesController.java

package com.example.demo;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("messages")
public class MessagesController {
	final List<Message> messages = new CopyOnWriteArrayList<>();
	
	@RequestMapping(method = RequestMethod.GET)
	
	public List<Message> getMessages(){
		return messages;
	}
	
	@RequestMapping(method = RequestMethod.POST)
	public Message postMessages(@RequestBody Message message) {
		messages.add(message);
		return message;
	}
}

### 画面遷移型アプリケーション
テンプレートエンジンとの連携ライブラリ(Starterプロジェクト、AutoConfigure)が用意
– Tymeleaf, FreeMarker, Groovy templates, Velocity, Mustacheなど

Tymeleafを使用する
pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

HelloController.java

package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HelloController {
	@RequestMapping("/hello")
	public String hello(Model model) {
		model.addAttribute("hello", "Hello World!");
		return "hello";
	}
}

/src/main/resources/templates/hello.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8" />
	<title></title>
</head>
<body>
	<p>
		<span th:text="${hello}">Hello!</span>
	</p>
</body>

src/main/resources