History
전체 코드 링크: click
- 24.08.28: 기존 편의 메서드 수정, 추가 및 테스트 코드 추가
테이블은 외래키 하나로 두 테이블 간 데이터가 조인되어 사용되지만, 객체에서 관계는 두 개의 참조가 존재하고 외래키를 관리하는 주인 쪽에서 데이터를 관리한다. 이때, 주인이 아닌 객체와 데이터 간에 불일치 문제가 발생할 수 있기 때문에 필요하다.
// Space
public void setBiCustomer(BiCustomer biCustomer) {
this.biCustomer = biCustomer;
}
@Test
@DisplayName("양방향 관계 설정이 누락되면 객체 일관성 깨짐")
public void createCustomerTest() {
BiCustomer customer = new BiCustomer("customer1");
BiSpace space1 = new BiSpace("space1");
BiSpace space2 = new BiSpace("space2");
space1.setBiCustomer(customer);
space2.setBiCustomer(customer);
// customer.getBiSpaces().add(space1);
// customer.getBiSpaces().add(space2);
assertThat(space1.getBiCustomer().getName()).isEqualTo(customer.getName());
assertThat(space2.getBiCustomer().getName()).isEqualTo(customer.getName());
assertThat(customer.getBiSpaces()).hasSize(2); // Error! Expected size: 2 but was: 0 in: []
}
// 불완전한 연관관계 편의 메서드
public void setBiCustomer(BiCustomer biCustomer) {
this.biCustomer = biCustomer;
biCustomer.getBiSpaces().add(this);
}
BiCustomer customer = new BiCustomer("customer1");
BiSpace space = new BiSpace("space1");
space.setBiCustomer(customer);
양방향 관계를 모두 설정하지 않으면 제대로 동작하지 않을까?
- 그렇지 않다. 연관관계 주인에서만 관계를 설정한 경우에도 외래키는 잘 등록된다. JPA는 데이터베이스에 등록된 외래키 정보를 보고, 연관관계를 구성할 수 있다.
@Test @Transactional @DisplayName("MySQL에 저장된 데이터를 조회할 때, JPA 연관관계는 자동으로 구성된다.") public void getSpaces() { BiSpace space = spaceRepository.findById(1L) .orElseThrow(() -> new RuntimeException("해당 아이디인 공간이 존재하지 않습니다.")); BiCustomer customer = customerRepository .findById(1L) .orElseThrow(() -> new RuntimeException("해당 아이디인 고객이 존재하지 않습니다.")); assertThat(space.getName()).isEqualTo("주니의 공간1"); assertThat(customer.getBiSpaces().size()).isEqualTo(2); }
- 헷갈릴 수 있는게, InMemoryDB에서 아래와 같은 코드를 실행하면 실패한다. 왜 실패할까?
@Test @Transactional @DisplayName("양방향 관계 설정이 누락되어도 영속성 컨텍스트에 등록되었다면 자유롭게 객채 그래프 탐색 가능") public void lookUpInPersistContext() { BiCustomer customer = new BiCustomer("customer1"); BiSpace space1 = new BiSpace("space1"); BiSpace space2 = new BiSpace("space2"); space1.setBiCustomer(customer); space2.setBiCustomer(customer); BiCustomer savedCustomer = customerRepository.save(customer); spaceRepository.save(space1); spaceRepository.save(space2); BiCustomer foundCustomer = customerRepository.findById(savedCustomer.getId()).get(); assertThat(foundCustomer.getName()).isEqualTo(customer.getName()); assertThat(foundCustomer.getBiSpaces().size()).isEqualTo(2); }
- 영속성 컨텍스트의 동작을 떠올려보자. 객체가 생성되고, 연관관계 주인에서만 관계가 설정된 상태로 영속성 컨텍스트에 저장된다. DB에서 조회하는 쿼리(findById)를 실행했지만, 영속성 컨텍스트에 양방향 관계 설정이 누락된 엔티티가 조회되며 테스트가 실패한다. DB에서 조회하기 전에 영속성 컨텍스트를 초기화하면 JPA가 외래키를 보고 만든 객체 관계가 포함된 엔티티가 영속성 컨텍스트에 새로 등록된다.
entityManager.clear();
// Space
public void setBiCustomer(BiCustomer biCustomer) {
this.biCustomer = biCustomer;
biCustomer.getBiSpaces().add(this);
}
// Space
public void setBiCustomer(BiCustomer biCustomer) {
if (this.biCustomer != null) {
this.biCustomer.getBiSpaces().remove(this); // 기존 참조 제거
}
this.biCustomer = biCustomer;
biCustomer.getBiSpaces().add(this);
}
// Consumer
public void addSpace(BiSpace biSpace) {
this.biSpaces.add(biSpace);
if (biSpace.getBiCustomer() != this) {
biSpace.setBiCustomer(this);
}
}
// Consumer
public void removeSpace(BiSpace biSpace) {
this.biSpaces.remove(biSpace);
if (biSpace.getBiCustomer() == this) {
biSpace.setBiCustomer(null);
}
}
@Test
@Transactional
@Rollback(false)
@DisplayName("Mysql DB에 데이터 삽입")
public void makeDummyData() {
BiCustomer biCustomer = new BiCustomer("juny");
BiSpace biSpace1 = new BiSpace("주니의 공간1");
BiSpace biSpace2 = new BiSpace("주니의 공간2");
biSpace1.setBiCustomer(biCustomer);
biSpace2.setBiCustomer(biCustomer);
spaceRepository.save(biSpace1);
spaceRepository.save(biSpace2);
customerRepository.save(biCustomer);
}
// insert into spaces_bi (bi_customer_id,name) values (NULL,'주니의 공간1');
// insert into spaces_bi (bi_customer_id,name) values (NULL,'주니의 공간2');
// insert into customers_bi (name) values ('juny');
// update spaces_bi set bi_customer_id=1,name='주니의 공간1' where id=3;
// update spaces_bi set bi_customer_id=1,name='주니의 공간2' where id=4;
...
customerRepository.save(biCustomer);
spaceRepository.save(biSpace1);
spaceRepository.save(biSpace2);
// insert into customers_bi (name) values ('juny');
// insert into spaces_bi (bi_customer_id,name) values (1,'주니의 공간1');
// insert into spaces_bi (bi_customer_id,name) values (1,'주니의 공간2');
@OneToMany(mappedBy = "biCustomer")
private Set<BiSpace> biSpaces = new HashSet<>();
// OneToMany 연관관계 편의 메서드, 고객 - 공간 [양방향]
public void addSpace(BiSpace biSpace) {
this.biSpaces.add(biSpace);
if (biSpace.getBiCustomer() != this) {
biSpace.setBiCustomer(this);
}
}
@OneToMany(mappedBy = "biCustomer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BiSpace> biSpaces = new ArrayList<>();
@Test
@Transactional
@Rollback(false)
@DisplayName("Mysql DB에 데이터 삽입")
public void makeDummyData() {
BiCustomer biCustomer = new BiCustomer("juny");
BiSpace biSpace1 = new BiSpace("주니의 공간1");
BiSpace biSpace2 = new BiSpace("주니의 공간2");
biSpace1.setBiCustomer(biCustomer);
biSpace2.setBiCustomer(biCustomer);
customerRepository.save(biCustomer);
}
insert into customers_bi (name) values ('juny');
쿼리만 실행된다.// Space
public void addSubCategory(SubCategory subCategory) {
this.subCategories.add(subCategory);
subCategory.addSpace(this);
}
// Space
public void removeSubCategory(SubCategory subCategory) {
subCategory.removeSpace(this);
}
// SubCategory
public void addSpace(Space space) {
this.spaces.add(space);
space.addSubCategory(this);
}
// SubCategory
public void removeSpace(Space space) {
space.removeSubCategory(this);
}
public class SpaceOptionId {
@Column(name = "space_id")
private Long spaceId;
@Column(name = "option_id")
private Long optionId;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SpaceOptionId that = (SpaceOptionId) o;
return Objects.equals(spaceId, that.spaceId) &&
Objects.equals(optionId, that.optionId);
}
@Override
public int hashCode() {
return Objects.hash(spaceId, optionId);
}
}
// 공간
public void addOption(Option option) {
SpaceOption spaceOption = new SpaceOption(this, option);
spaceOptions.add(spaceOption);
option.getSpaceOptions().add(spaceOption);
}
// 공간
public void removeOption(Option option) {
SpaceOption spaceOption = new SpaceOption(this, option);
option.getSpaceOptions().remove(spaceOption);
spaceOption.setSpace(null);
spaceOption.setOption(null);
}
// 옵션
public void addSpace(Space space) {
SpaceOption spaceOption = new SpaceOption(space, this);
spaceOptions.add(spaceOption);
space.getSpaceOptions().add(spaceOption);
}
// 옵션
public void removeSpace(Space space) {
SpaceOption spaceOption = new SpaceOption(space, this);
space.getSpaceOptions().remove(spaceOption);
spaceOption.setSpace(null);
spaceOption.setOption(null);
}
// 유저
public void setHost(Host host) {
this.host = host;
if (host != null) {
host.setUser(this);
}
}
// 호스트
public void setUser(User user) {
this.user = user;
if (user != null) {
user.setHost(this);
}
}
// 고객
public void addSpace(UniSpace uniSpace) {
this.uniSpaces.add(uniSpace);
}
@Test
@Transactional
@Rollback(false)
public void createCustomerTest() {
UniCustomer uniCustomer = new UniCustomer("juny");
UniSpace uniSpace1 = new UniSpace("주니의 공간1");
UniSpace uniSpace2 = new UniSpace("주니의 공간1");
spaceRepository.save(uniSpace1);
spaceRepository.save(uniSpace2);
uniCustomer.addSpace(uniSpace1);
uniCustomer.addSpace(uniSpace2);
customerRepository.save(uniCustomer);
}
}
// insert into customers (name) values ('juny');
// update spaces set customer_id=2 where id=1;
// update spaces set customer_id=2 where id=2;
// 공간
public void setProperty(Property property) {
this.property = property;
}
// 공간
public void addSubCategory(SubCategory subCategory) {
this.subCategories.add(subCategory);
}
// 공간
public void removeSubCategory(SubCategory subCategory) {
this.subCategories.remove(subCategory);
}
// 공간
public void addOption(Option option) {
SpaceOption spaceOption = new SpaceOption(this, option);
spaceOptions.add(spaceOption);
}
// 공간
public void removeOption(Option option) {
SpaceOption spaceOption = new SpaceOption(this, option);
spaceOptions.remove(spaceOption);
spaceOption.setSpace(null);
spaceOption.setOption(null);
}
// 유저
public void setHost(Host host) {
this.host = host;
}