일단 모든 엔티티의 생성시간만 공통으로 관리하기 위해 BaseEntity를 작성했습니다.
이후 수정시간 등이 공통으로 필요하다 생각되면 추가할 예정입니다.
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
protected LocalDateTime createAt;
}
모든 엔티티의 기본생성자는 access level
을 PROTECTED
로 하여 막아주기로 하였고 Getter
정도는 열어주기로 했습니다.
Oauth2
를 통해 본인인증만을 수행할 예정이기 때문에 일반 사용자에게 password는 필요없지만 ADMIN
은 폼 로그인을 사용할 예정이기 때문에 추가했습니다.
Oauth2
를 통해 얻어올 수 있는 정보는 그대로 넣어주고 받을 수 없는 정보는 추가로 폼을 통해 받을 예정입니다.
모든 엔티티, DTO, 요청객체 등 모두 Builder
패턴을 이용했습니다.
Builder
패턴이 실수를 방지하고 인스턴스 생성에 보다 명확성을 부여해준다 생각해서 이번 프로젝트에 모두 적용해볼 생각입니다.
package com.threefam.reserve.domain.entity;
import com.threefam.reserve.domain.value.Gender;
import com.threefam.reserve.domain.value.Role;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
@Getter
public class User extends BaseEntity{
@Column(name = "user_id")
@Id @GeneratedValue
private Long id;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Gender gender;
@Column(nullable = false)
private Integer age;
@Column(nullable = false)
private String address;
@Column(nullable = false)
private String detailAddress;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@Builder(builderMethodName = "createUser")
public User(String email, String password, String name,
Gender gender, Integer age, String address, String detailAddress, Role role) {
this.email = email;
this.password = password;
this.name=name;
this.gender = gender;
this.age = age;
this.address = address;
this.detailAddress = detailAddress;
this.role = role;
this.createAt = LocalDateTime.now();
}
}
HospitalEntity
객체에서 예약가능날짜를 객체 그래프로 조회하기 위해 양방향으로 설정해주었습니다.
(양방향 관계에서 side-effect를 방지하기 위해 편의 메서드를 정의했습니다. 어느 한쪽에는 데이터가 세팅되지 않는 .. 그런 ...)
모든 ToOne
관계는 Lazy
로 설정해주었습니다. ( Eager
No! )
JPA에서 Boolean
타입을 사용하기 위해 @Type
어노테이션 사용
백신 엔티티 또한 HospitalEntity
객체에서 편하게 조회하기 위해 양방향으로 설정했습니다.
병원 엔티티와 백신 엔티티의 생명주기를 같이하기 위해 cascade
와 orphanRemoval
사용
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "hospital")
@Getter
public class Hospital extends BaseEntity{
@Id @GeneratedValue
@Column(name = "hospital_id")
private Long id;
@Column(name = "hospital_name")
private String hospitalName;
// 양방향
@OneToMany(mappedBy = "hospital",cascade = CascadeType.ALL)
@JsonIgnoreProperties({"hospital"})
private List<AvailableDate> availableDates = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "admin_id")
private Admin admin;
public void setAdmin(Admin admin) {
this.admin = admin;
admin.getHospitals().add(this);
}
@Column(nullable = false)
private String address;
@Column(nullable = false)
private String detailAddress;
@Column(name = "total_quantity")
private Integer totalQuantity;
public void cancel() {
this.totalQuantity++;
}
@Column(name = "date_accept")
private Integer dateAccept;
@Column(name = "time_accept")
private Integer timeAccept;
public void setTotalVaccineQuantity(Integer qty) {
this.totalQuantity = qty;
}
public void removeStock() {
int restStock=this.totalQuantity-1;
if(restStock==0){
setEnabled(false);
}
if(restStock<0){
throw new NotEnoughStockException("예약 가능한 수량이 부족합니다.");
}
this.totalQuantity=restStock;
}
public void updateDateAccept(Integer dateAccept){this.dateAccept=dateAccept;}
public void updateTimeAccept(Integer timeAccept){this.timeAccept=timeAccept;}
// true: y, false: n
@Type(type = "yes_no")
private Boolean enabled = true; // 예약 가능 여부
@OneToMany(mappedBy = "hospital", cascade = CascadeType.PERSIST, orphanRemoval = true)
@JsonIgnoreProperties({"hospital"})
private List<Vaccine> vaccines = new ArrayList<>();
public void setEnabled(boolean flag) {
this.enabled = flag;
}
// 연관관계 편의 메서드
private void addAdmin(Admin admin) {
this.admin = admin;
admin.getHospitals().add(this);
}
@Builder(builderMethodName = "createHospital")
public Hospital(String hospitalName, String address, String detailAddress,Integer dateAccept,Integer timeAccept) {
this.hospitalName = hospitalName;
this.address = address;
this.detailAddress = detailAddress;
this.createAt = LocalDateTime.now();
this.dateAccept=dateAccept;
this.timeAccept=timeAccept;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "vaccine")
@Getter
public class Vaccine extends BaseEntity{
@Id @GeneratedValue
@Column(name = "vaccine_id")
private Long id;
@Column(name = "vaccine_name", nullable = false)
private String vaccineName;
@Column(nullable = false)
private Integer quantity;
public void cancel() {
this.quantity++;
this.enabled=true;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "hospital_id")
private Hospital hospital;
@Type(type="yes_no")
private boolean enabled = true;
public void setEnabled(boolean flag) {
this.enabled = flag;
}
@Builder(builderMethodName = "createVaccine")
public Vaccine(String vaccineName, Integer quantity) {
this.vaccineName = vaccineName;
this.quantity = quantity;
this.createAt = LocalDateTime.now();
}
// 연관관계 편의 메서드
public void addHospital(Hospital hospital) {
this.hospital = hospital;
hospital.getVaccines().add(this);
}
//==비즈니스 로직==//
//예약 취소 시, 사용
public void addStock(){
this.quantity+=1;
}
//예약 시, 사용
public void removeStock(){
int restStock=this.quantity-1;
if(restStock==0){
setEnabled(false);
}
if(restStock<0){
throw new NotEnoughStockException("예약 가능한 수량이 부족합니다.");
}
this.quantity=restStock;
}
//병원 수정 시, 사용
public void updateVaccineQty(Integer quantity){
this.quantity=quantity;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "available_date")
@Getter
public class AvailableDate {
@Id
@GeneratedValue
@Column(name = "available_data_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "hospital_id")
private Hospital hospital;
@Column(nullable = false)
private String date;
// 양방향
@OneToMany(mappedBy = "availableDate", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnoreProperties({"availableDate"})
private List<AvailableTime> availableTimes = new ArrayList<>();
// 일일 수용 가능 인원
@Column(name = "accept_count")
private Integer acceptCount;
public void cancel() {
this.acceptCount++;
this.enabled=true;
}
public void decreaseCount() {
int restStock=this.acceptCount-1;
if(restStock==0){
setEnabled(false);
}
if(restStock<0){
throw new NotEnoughStockException("예약 가능한 수량이 부족합니다.");
}
this.acceptCount=restStock;
}
@Type(type = "yes_no")
private Boolean enabled = true;
public void setEnabled(boolean flag) {
this.enabled = flag;
}
// 양방향 연관관계 편의 메서드
public void addHospital(Hospital hospital) {
this.hospital = hospital;
hospital.getAvailableDates().add(this);
}
@Builder(builderMethodName = "createAvailableDate")
public AvailableDate(String date,Integer acceptCount){
this.date=date;
this.acceptCount=acceptCount;
}
//병원 상세내용 수정 시, count update
public void updateAcceptCount(Integer acceptCount){
this.acceptCount=acceptCount;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "available_time")
@Getter
public class AvailableTime {
@Id
@GeneratedValue
@Column(name = "available_time_id")
private Long id;
@Column(nullable = false)
private int time;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "available_date_id")
private AvailableDate availableDate;
// 한 타임동안 수용 가능한 인원
private Integer acceptCount;
public void cancel() {
this.acceptCount++;
this.enabled=true;
this.availableDate.cancel();
}
public void decreaseCount() {
int restStock=this.acceptCount-1;
if(restStock==0){
setEnabled(false);
}
if(restStock<0){
throw new NotEnoughStockException("예약 가능한 수량이 부족합니다.");
}
this.availableDate.decreaseCount();
this.acceptCount=restStock;
}
@Type(type = "yes_no")
private Boolean enabled = true;
public void setEnabled(boolean flag) {
this.enabled = flag;
}
// 양방향 연관관계 편의 메서드
public void addAvailableDate(AvailableDate availableDate) {
this.availableDate = availableDate;
availableDate.getAvailableTimes().add(this);
}
private void setAvailableTime(int time) {
this.time = time;
}
@Builder(builderMethodName = "createAvailableTime")
public AvailableTime(int time, Integer acceptCount) {
this.time = time;
this.acceptCount = acceptCount;
}
public void updateAcceptCount(Integer acceptCount){
this.acceptCount=acceptCount;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "reserve_item")
@Getter
public class ReserveItem extends BaseEntity {
@Column(name = "reserve_item_id")
@Id
@GeneratedValue
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "hospital_id")
private Hospital Hospital;
@Enumerated(value = EnumType.STRING)
private ReserveStatus status = ReserveStatus.COMP;
@Column(nullable = false)
private String vaccineName;
@Column(nullable = false)
private String reserveDate;
@Column(nullable = false)
private int reserveTime;
@Builder(builderMethodName = "createReserveItem")
public ReserveItem(
User user, Hospital Hospital, ReserveStatus status, String reserveDate, int reserveTime, String vaccineName) {
this.user = user;
this.Hospital = Hospital;
this.status = status;
this.reserveDate = reserveDate;
this.reserveTime = reserveTime;
this.vaccineName = vaccineName;
this.createAt = LocalDateTime.now();
}
//==비즈니스 로직==//
//예약 날짜 및 예약 시간 update
public void updateDateAndTime(String reserveDate,int reserveTime){
this.reserveDate=reserveDate;
this.reserveTime=reserveTime;
}
}
admin
에서 객체 그래프로 병원을 조회할 수 있도록 양방향 설정
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "admin")
@Getter
public class Admin extends BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "admin", cascade = CascadeType.ALL)
@JsonIgnoreProperties({"admin"})
private List<Hospital> hospitals = new ArrayList<>();
@Builder(builderMethodName = "createAdmin")
public Admin(String name) {
this.name = name;
}
}