현재 코드
① 대출 기능
Book책 정보를 가져온다.
UserLoanHistoryRepository검증한다.
User사용자 정보를 가져온다.
→BookService
→UserLoanHistory에 기록을 만들어 저장한다.② 반납 기능
User사용자 정보를 가져온다.
UserLoanHistory대출 기록을 가져와 반납으로 처리한다.
→BookService
User ⇔ UserLoanHistory 사용자 정보와 대출 기록을 가져와 바로 대출 및 반납을 처리한다.
⇒ BookService
선행 조건 : User 와 UserLoanHistory 가 서로를 알아야 한다.
@ManyToOne 어노테이션@Entity public class UserLoanHistory
{
@ManyToOne
private User user;
}
↪ User 객체를 UserLoanHistory 객체의 멤버 필드로 사용할 수 있다.
@OneToMany 어노테이션@Entity
public class User
{
@OneToMany
private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
}
↪ UserLoanHistory 객체 타입을 List 로 사용할 수 있다.
연결되어 있는 Table을 보았을 때 누가 관계의 주도권을 갖고 있는가
// user 테이블
CREATE TABLE user(
user_id BIGINT AUTO_INCREMENT NOT NULL,
user_name VARCHAR(25) NOT NULL,
user_age INT NOT NULL,
PRIMARY KEY(user_id)
);
// user_loan_history 테이블
CREATE TABLE user_loan_history(
load_id BIGINT AUTO_INCREMENT,
user_id BIGINT,
book_name VARCHAR(255),
is_return TINYINT(1),
PRIMARY KEY(load_id)
);
↪ user_loan_history 테이블이 주도권을 갖고 있다.
mappedBy 옵션↪ 연관관계의 주인이 아닌 쪽에 mappedBy 옵션을 달아줘야 한다.
@OneToMany(mappedBy = "user")
@Entity
public class User
{
@OneToMany(mappedBy = "user")
private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
}
↪ 연관관계의 주인의 값이 설정되어야만 진정한 데이터가 저장된다.
CREATE TABLE person (
person_id BIGINT AUTO_INCREMENT PRIMARY KEY,
person_name VARCHAR(255),
address_id BIGINT
);
CREATE TABLE address(
address_id BIGINT AUTO_INCREMENT PRIMARY KEY,
city VARCHAR(255),
street VARCHAR(255)
);
↪ person이 주도권을 가지고 있다. person이 연관관계의 주인
Person
@Entity
public class Person
{
// [멤버 필드]
@OneToOne
private Address address;
}
Address
@Entity
public class Address
{
// [멤버 필드]
@OneToOne(mappedBy = "address")
private Person person;
}
Setter 가 호출되어야만@Transactioinal
public void savePerson()
{
Person person = personRepository.save(new Person());
Address address = addressRepository.save(new Address());
person.setAddress(address); // 정상 반영
// address.setPerson(person); -> null
}
mappedBy 옵션을 사용한다.Setter 가 호출되어야만 테이블을 연결할 수 있다. @Transactioinal
public void savePerson()
{
Person person = personRepository.save(new Person());
Address address = addressRepository.save(new Address());
person.setAddress(address); // 정상 반영
System.out.println(address.getPerson()); // null
}
Setter 한 번에 둘을 같이 이어준다.!Person
@Entity
public class Person
{
// [Setter]
public void setAddress(Address address)
{
this.address = address;
this.address.setPerson(this); // -> Setter끼리 연결
}
}
Address
@Entity
public class Address
{
// [Setter]
public void setPerson(Person person)
{
this.person = person;
}
// [Getter]
public Person getPerson()
{
return this.person;
}
}
PersonService
@Transactoinal void savePerson()
{
Person person = personRepository.save(new Person());
Address address = addressRepository.save(new Address());
person.setAddress(address); // 테이블 간 연결
address.getPerson(); // 객체끼리 연결
}
@ManyToOne과 @OneToMany@ManyToOne을 단방향으로 사용할 수 있다.User 클래스에서 private List<UserLoanHistory> userLoanHistoryList를 생략할 수 있다.@ManyToMany기타 옵션
JoinColumn어노테이션
cascade옵션
orphanRemoval옵션
@JoinColumn@Column 어노테이션과 역할은 유사하나, @JoinColumn은 연관관계의 주인에게 활용할 수 있는 어노테이션이다.cascade 옵션예시) User가 책 1과 책 2를 빌렸을 때, User 객체를 삭제하면 → User만 삭제되고 UserLoanHistory는 남아있게 된다.
@OneToMany(mappedBy = "user", cacade = CascadeType.ALL)
private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
orphanRemoval 옵션@Transactional
public void deleteUserHistory()
{
User user = userRepository.findByName("정수현")
.orElseThrow(IllegalArgumentException::new);
user.removeOneHistory();
}
public void removeOneHistory()
{
userLoanHistoryList.removeIf(history -> "책1".equals(history.getBookName()));
}
↪ 데이터베이스 상에 아무런 변화 없다.
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
상대 테이블을 가리키는 테이블이 연관관계의 주인이다.
연관관계의 주인이 아닌 객체는 mappedBy를 통해 주인에게 매여 있음을 표시해 주어야 한다.
양쪽 모두 연관관계를 갖고 있을 때는 양쪽 모두 한 번에 맺어주는 게 좋다.
예시) 한쪽의 Setter를 부를 때 양쪽이 연결되게끔 양쪽 객체 클래스에 모두 Setter를 이어주도록 구현하는게 중요하다.
cascade 옵션을 활용하면 저장이나 삭제를 할 때 연관관계에 놓인 테이블까지 함께 연관관계에 놓인 테이블까지 함께 저장 또는 삭제가 이루어진다.
orphanRemoval 옵션을 활용하면, 연관관계가 끊어진 데이터를 자동으로 제거해준다.
User 클래스에서 일반 메서드로 loanBook 과 returnBook 기능 함수를 구현하였다.
User
@Entity
public class User
{
// [일반 메서드]
// 대출 기능
public void loanBook(String bookName)
{
this.userLoanHistoryList.add(new UserLoanHistory(this, bookName, false));
}
// 반납 기능
public void returnBook(String bookName)
{
// 1) 대출 기록을 읽어들여 bookName에 일치하는 기록을 찾는다.
UserLoanHistory targetHistory = this.userLoanHistoryList.stream() // 리스트를 한줄씩 읽어들인다.
.filter(history -> history.getBookName().equals(bookName)) // 책이름과 일치하는 기록을 가져온다.
.findFirst() // 첫번째에 해당하는 기록을 반환한다.
.orElseThrow(IllegalArgumentException::new);
// 해당 기록을 찾았으면 반납처리한다.
targetHistory.doReturn();
}
}
BookService
@Transactional
public void selectReturn(BookReturnRequest request)
{
// 1) User 정보를 가져온다.
User user = userRepository.findByUserName(request.getUserName())
.orElseThrow(IllegalArgumentException::new);
// 2) UserLoanHistory에서 반납을 처리한다.
user.returnBook(request.getBookName());
}
⇒ JPA의 연관관계 옵션을 활용해서 최대한 도메인들끼리 직접 협력할 수 있도록 코드를 수정하였다.
User와 UserLoanHistory 클래스를 호출하는게 아닌,User만 가져왔다가 UserLoanHistory가 필요한 순간에 호출한다.@OneToMany의 fetch 옵션