▸ 오늘의 코드카타
▸ Raw JPA 연관관계 매핑
▸ 복합키
▸ Cascade(영속성 전이)
▸ orphanRemoval(고아 객체 제거)
▸ Fetch(조회시점)
2024년 3월 7일 - [프로그래머스 - 자바(JAVA)] 34 : 이웃한 칸 | 올바른 괄호
@OneToMany
가 단방향으로 쓰이면 문제가 발생할 수 있다.@ManyToOne
어노테이션과 주로 함께 쓰인다. (조인대상 컬럼 지정기능을 안쓸거면 생략해도 됨)@Column
의 속성과 같음// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_CHANNEL")
public class Channel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Type type;
public enum Type {
PUBLIC, PRIVATE;
}
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public Channel (String name, Type type) {
this.name = name;
this.type = type;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "channel")
private Set<Thread> threads = new LinkedHashSet<>();
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
public void addThread(Thread thread) {
this.threads.add(thread);
}
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
@OneToMany(mappedBy = "channel")
를 통해 Thread 와 매핑했다.// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_THREAD")
public class Thread {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Column(length = 500)
private String message;
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public Thread (String message) {
this.message = message;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@ManyToOne
@JoinColumn(name = "channel_id")
private Channel channel;
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
public void setChannel (Channel channel) {
this.channel = channel;
channel.addThread(this);
}
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
@ManyToOne @JoinColumn(name = "channel_id")
를 통해 Channel과 매핑했다.@ManyToOne
과 @JoinColumn
은 같이 사용된다.RawJPA는 @PersistenceContext
를 주입받아와서 EntityManager를 직접 사용하고 있다.
@Repository
public class ChannelRepository {
@PersistenceContext
EntityManager entityManager;
public Channel insertChannel (Channel channel) {
entityManager.persist(channel);
return channel;
}
public Channel selectChannel (Long id) {
return entityManager.find(Channel.class, id);
}
}
@Repository
public class ThreadRepository {
@PersistenceContext
EntityManager entityManager;
public Thread insertThread (Thread thread) {
entityManager.persist(thread);
return thread;
}
public Thread selectThread (Long id) {
return entityManager.find(Thread.class, id);
}
}
@SpringBootTest
와 @Transactional
, @Rollback
을 사용하는 의미는 무엇일까?@SpringBootTest
@Transactional
@Transactional
을 표시하면 테스트 작업이 트랜잭션 컨텍스트 내에서 실행된다.@Rollback
@Rollback(value = false)
는 테스트에서 기본 롤백 동작을 재정의하는 데 사용된다. 기본적으로 Spring은 각 테스트 메서드 실행 후 트랜잭션을 롤백한다.@Rollback(value = false)
를 설정하면 트랜잭션이 롤백되지 않고 테스트 중에 발생한 변경 사항이 데이터베이스에 유지된다.@SpringBootTest
@Transactional
@Rollback(value = false)
class ChannelRepositoryTest {
@Autowired
private ChannelRepository channelRepository;
@Test
void 삽입_조회_성공() {
// given
Channel newChannel = Channel.builder().name("newGrooup").build();
// when
Channel savedChannel = channelRepository.insertChannel(newChannel);
// then
Channel foundChannel = channelRepository.selectChannel(savedChannel.getId());
assert foundChannel.getId().equals(savedChannel.getId());
}
}
@SpringBootTest
@Transactional
@Rollback(value = false)
class ThreadRepositoryTest {
@Autowired
private ThreadRepository threadRepository;
@Test
void Thread_삽입_조회_성공() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
Thread newThread = Thread.builder().message("new-message").build();
newThread.setChannel(newChannel);
// when
Thread savedThread = threadRepository.insertThread(newThread);
// then
Thread foundThread = threadRepository.selectThread(savedThread.getId());
assert foundThread.getId().equals(savedThread.getId());
}
}
오류 발생
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : me.springstudy.jpastudy.thread.Thread.channel -> me.springstudy.jpastudy.channel.Channel
⇒ channel 이 flushing 이 안되어있다고 나옴
newChannel을 만들었는데 flushing이 안됨
해결방법
- save 상태에서 persist 상태로 바꾸기 → 아래 코드 추가
- 같이 영속성 컨텍스트로 넘겨줘야함
@Autowired private ChannelRepository channelRepository; @Test void Thread_삽입_조회_성공() { // when Channel savedChannel = channelRepository.insertChannel(newChannel); }
@SpringBootTest
@Transactional
@Rollback(value = false)
class ThreadRepositoryTest {
@Autowired
private ThreadRepository threadRepository;
@Autowired
private ChannelRepository channelRepository;
@Test
void Thread_삽입_조회_성공() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
Thread newThread1 = Thread.builder().message("new-message").build();
Thread newThread2 = Thread.builder().message("new-message1").build();
newThread1.setChannel(newChannel);
newThread2.setChannel(newChannel);
// when
Thread savedThread1 = threadRepository.insertThread(newThread1);
Thread savedThread2 = threadRepository.insertThread(newThread2);
Channel savedChannel = channelRepository.insertChannel(newChannel);
// then
Channel foundChannel = channelRepository.selectChannel(savedChannel.getId());
assert foundChannel.getThreads().containsAll(Set.of(savedThread1, savedThread2));
}
}
@ManyToMany
를 사용하는 것이 아니라 중간 테이블을 새로 하나 만든다.@OneToMany
) > MappingTable(@ManyToOne
, @ManyToOne
) > TableB(@OneToMany
)// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_USER")
public class User {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(length = 25)
private String username;
@Column(length = 25)
private String password;
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public User (String username, String password) {
this.username = username;
this.password = password;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "user")
Set<UserChannel> userChannels = new LinkedHashSet<>();
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_CHANNEL")
public class Channel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Type type;
public enum Type {
PUBLIC, PRIVATE;
}
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public Channel (String name, Type type) {
this.name = name;
this.type = type;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "channel")
private Set<Thread> threads = new LinkedHashSet<>();
@OneToMany(mappedBy = "channel")
private Set<UserChannel> userChannels = new LinkedHashSet<>();
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
public void addThread(Thread thread) {
this.threads.add(thread);
}
public UserChannel joinUser (User user) {
UserChannel userChannel = UserChannel.builder().user(user).channel(this).build();
this.userChannels.add(userChannel);
user.getUserChannels().add(userChannel);
return userChannel;
} // 이 부분 잘 생각해보기
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_USERCHANNEL")
public class UserChannel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public UserChannel(User user, Channel channel) {
this.user = user;
this.channel = channel;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@ManyToOne
@JoinColumn(name = "user_id")
User user;
@ManyToOne
@JoinColumn(name = "channel_id")
Channel channel;
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
@Repository
public class UserChannelRepository {
@PersistenceContext
EntityManager entityManager;
public UserChannel insertUserChannel (UserChannel userchannel) {
entityManager.persist(userchannel);
return userchannel;
}
public UserChannel selectUserChannel (Long id) {
return entityManager.find(UserChannel.class, id);
}
}
@SpringBootTest
@Transactional
@Rollback(value = false)
class UserChannelRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private ChannelRepository channelRepository;
@Autowired
private UserChannelRepository userChannelRepository;
@Test
void userJoinChannelTest() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
User newUser = User.builder().username("new-user").password("new-pass").build();
UserChannel newUserChannel = newChannel.joinUser(newUser);
// when
Channel savedChannel = channelRepository.insertChannel(newChannel);
User savedUser = userRepository.insertUser(newUser);
UserChannel savedUserChannel = userChannelRepository.insertUserChannel(newUserChannel);
// then
Channel foundChaneel = channelRepository.selectChannel(savedChannel.getId());
assert foundChaneel.getUserChannels().stream()
.map(UserChannel::getChannel)
.map(Channel::getName)
.anyMatch(name -> name.equals(newChannel.getName()));
}
}
@IdClass
를 활용하는 복합키는 복합키를 사용할 엔티티 위에 @IdClass(식별자 클래스) 사용예시 코드 - @IdClass
UserChannel
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@IdClass(UserChannelId.class)
@Table(name = "TB_USERCHANNEL")
public class UserChannel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public UserChannel(User user, Channel channel) {
this.user = user;
this.channel = channel;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@Id
@ManyToOne
@JoinColumn(name = "user_id")
User user;
@Id
@ManyToOne
@JoinColumn(name = "channel_id")
Channel channel;
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
UserChannelId
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserChannelId implements Serializable {
private Long user; // UserChannel 의 user 필드명과 동일해야함
private Long channel; // UserChannel 의 channel 필드명과 동일해야함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
}
@EmbeddedId
를 활용하는 복합키는 복합키 위에 @EmbeddedId 사용@EmbeddedId
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_USERCHANNEL")
public class UserChannel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@EmbeddedId
private UserChannelId userChannelId;
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public UserChannel(User user, Channel channel) {
this.user = user;
this.channel = channel;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@ManyToOne
@MapsId("user_id")
User user;
@ManyToOne
@MapsId("channel_id")
Channel channel;
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class UserChannelId implements Serializable {
@Serial
private static final long serialVersionUID = 932813899396663626L;
@Column(name = "user_id")
private Long userId;
@Column(name = "channel_id")
private Long channelId;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUserId(), userChannelId.getUserId()) && Objects.equals(
getChannelId(), userChannelId.getChannelId());
}
@Override
public int hashCode() {
return Objects.hash(getUserId(), getChannelId());
}
}
@OneToMany
가 있는 쪽 또는 @OneToOne
도 가능사용 위치
@OneToMany
또는 @OneToOne
에서 사용 - 부모 엔티티사용법
Parent parent1 = em.find(Parent.class, parent.getId());
parent1.getChildList().remove(0); // delete 쿼리나간다.
그렇다면 Cascade.REMOVE 와 orphanRemoval 차이점은 무엇인가?
Cascade.REMOVE의 경우 일에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 다에 해당하는 자식 엔티티들이 삭제되는 것이다.
orphanRemoval=true는 위 케이스도 포함하며,일에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 다에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하면 된다.
즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻
⇒ 부가설명
orphanRemoval=true
를 사용하는 경우에는 부모 엔티티의 리스트에서 요소를 삭제할 때 해당 요소에 연관된 자식 엔티티가 자동으로 삭제된다. 이를 통해 리스트에서 요소가 제거될 때 관련된 자식 엔티티도 함께 삭제되므로 데이터베이스에서 쓰레기 데이터가 생성되는 것을 방지할 수 있다.예시
@Entity public class Parent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) private List<Child> children = new ArrayList<>(); // getters and setters } @Entity public class Child { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne private Parent parent; // getters and setters }
위의 코드에서
Parent
엔티티에는Child
엔티티와의 일대다 관계가 있다. 이 때orphanRemoval=true
를 설정하면 부모 엔티티에서 자식 엔티티를 삭제할 때 관련된 자식 엔티티가 자동으로 삭제된다.Parent parent = entityManager.find(Parent.class, parentId); Child child = parent.getChildren().get(0); // 첫 번째 자식 엔티티 가져오기 parent.getChildren().remove(child); // 부모 엔티티에서 자식 엔티티 제거 // 자식 엔티티는 자동으로 삭제됨 (orphanRemoval=true 때문에)
위의 코드에서
parent
엔티티에서 자식 엔티티를 제거하면 자식 엔티티가 자동으로 삭제된다. 이것이orphanRemoval=true
가 리스트 요소로써의 영속성 전이를 해준다는 의미이다.
Gpt
CascadeType.REMOVE
와orphanRemoval
은 둘 다 JPA에서 관계를 관리하는 데 사용되는 기능이지만, 목적과 동작 방식에 차이가 있습니다.
- CascadeType.REMOVE:
CascadeType.REMOVE
는 부모 엔티티가 삭제될 때 자식 엔티티도 함께 삭제되도록 지정하는 기능입니다.- 예를 들어, 부모 엔티티를 삭제할 때 자식 엔티티도 함께 삭제하려면 CascadeType.REMOVE를 사용하여 연관 관계를 설정할 수 있습니다.
- 이것은 부모 엔티티의 삭제가 자식 엔티티에 대한 캐스케이드 삭제를 유발합니다.
- orphanRemoval:
orphanRemoval
은 연관된 엔티티가 더 이상 부모 엔티티에 속하지 않을 때 자동으로 삭제되도록 지정하는 기능입니다.- 이것은 단순히 부모 엔티티의 삭제와는 관련이 없으며, 자식 엔티티가 부모와의 관계에서 분리되면 자동으로 삭제됩니다.
- orphanRemoval을 사용하면 부모와의 연관 관계가 해제될 때 자식 엔티티를 자동으로 삭제하여 데이터베이스에서 쓰레기 데이터를 방지할 수 있습니다.
따라서 CascadeType.REMOVE은 부모 엔티티의 삭제에 따라 자식 엔티티가 함께 삭제되는 것을 제어하는 반면, orphanRemoval은 부모와의 연관 관계가 끊길 때 자식 엔티티를 삭제하는 것을 제어합니다.
옵션
리펙토링
위의 코드를 보면 사용하지 않는 변수가 존재하는 것을 알 수 있다.
Cascade 설정을 통하여 위와 같은 코드를 조금 더 간결하게 짜고자 했다.
UserChannel을 저장하지 않더라도 저장이 되도록 하기
Channel
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_CHANNEL")
public class Channel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Type type;
public enum Type {
PUBLIC, PRIVATE;
}
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public Channel (String name, Type type) {
this.name = name;
this.type = type;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "channel")
private Set<Thread> threads = new LinkedHashSet<>();
@OneToMany(mappedBy = "channel", cascade = CascadeType.ALL)
private Set<UserChannel> userChannels = new LinkedHashSet<>();
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
public void addThread(Thread thread) {
this.threads.add(thread);
}
public UserChannel joinUser (User user) {
UserChannel userChannel = UserChannel.builder().user(user).channel(this).build();
this.userChannels.add(userChannel);
user.getUserChannels().add(userChannel);
return userChannel;
}
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
UserChannel
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// jpa
@Entity
@Table(name = "TB_USERCHANNEL")
public class UserChannel {
/**
* 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
/**
* 생성자 - 약속된 형태로만 생성가능하도록 합니다.
*/
@Builder
public UserChannel(User user, Channel channel) {
this.user = user;
this.channel = channel;
}
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@ManyToOne
@JoinColumn(name = "user_id")
User user;
@ManyToOne
@JoinColumn(name = "channel_id")
Channel channel;
/**
* 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
*/
/**
* 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
*/
}
UserChannelRepositoryTest
@Test
void userJoinChannelWithCascadeTest() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
User newUser = User.builder().username("new-user").password("new-pass").build();
newChannel.joinUser(newUser);
// when
Channel savedChannel = channelRepository.insertChannel(newChannel);
User savedUser = userRepository.insertUser(newUser);
// then
Channel foundChaneel = channelRepository.selectChannel(savedChannel.getId());
assert foundChaneel.getUserChannels().stream()
.map(UserChannel::getChannel)
.map(Channel::getName)
.anyMatch(name -> name.equals(newChannel.getName()));
}
orphanRemoval 설정
ThreadRepositoryTest - orphanRemoval 설정 x
@Test
void deleteThreadWithoutOrphanRemovalTest() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
Thread newThread1 = Thread.builder().message("new-message").build();
Thread newThread2 = Thread.builder().message("new-message1").build();
newThread1.setChannel(newChannel);
newThread2.setChannel(newChannel);
Thread savedThread1 = threadRepository.insertThread(newThread1);
Thread savedThread2 = threadRepository.insertThread(newThread2);
Channel savedChannel = channelRepository.insertChannel(newChannel);
// when
Channel foundChannel = channelRepository.selectChannel(savedChannel.getId());
foundChannel.getThreads().remove(savedThread1);
}
ThreadRepositoryTest - orphanRemoval 설정 o
Channel
@OneToMany(mappedBy = "channel", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Thread> threads = new LinkedHashSet<>();
ThreadRepositoryTest
@Test
void deleteThreadWithoutOrphanRemovalTest() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
Thread newThread1 = Thread.builder().message("new-message").build();
Thread newThread2 = Thread.builder().message("new-message1").build();
newThread1.setChannel(newChannel);
newThread2.setChannel(newChannel);
Thread savedThread1 = threadRepository.insertThread(newThread1);
Thread savedThread2 = threadRepository.insertThread(newThread2);
Channel savedChannel = channelRepository.insertChannel(newChannel);
// when
Channel foundChannel = channelRepository.selectChannel(savedChannel.getId());
foundChannel.getThreads().remove(savedThread1);
}
User 와 Channel 이 다대다 관계 → 여기에도 적용할 수 있음
UserChannelRepository 가 없어도 됨
User
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
Set<UserChannel> userChannels = new LinkedHashSet<>();
Channel
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "channel", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Thread> threads = new LinkedHashSet<>();
@OneToMany(mappedBy = "channel", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserChannel> userChannels = new LinkedHashSet<>();
UserChannelRepositoryTest
@SpringBootTest
@Transactional
@Rollback(value = false)
class UserChannelRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private ChannelRepository channelRepository;
@Test
void userJoinChannelTest() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
User newUser = User.builder().username("new-user").password("new-pass").build();
UserChannel newUserChannel = newChannel.joinUser(newUser);
// when
Channel savedChannel = channelRepository.insertChannel(newChannel);
User savedUser = userRepository.insertUser(newUser);
// then
Channel foundChaneel = channelRepository.selectChannel(savedChannel.getId());
assert foundChaneel.getUserChannels().stream()
.map(UserChannel::getChannel)
.map(Channel::getName)
.anyMatch(name -> name.equals(newChannel.getName()));
}
@Test
void userJoinChannelWithCascadeTest() {
// given
Channel newChannel = Channel.builder().name("new-channel").build();
User newUser = User.builder().username("new-user").password("new-pass").build();
newChannel.joinUser(newUser);
// when
Channel savedChannel = channelRepository.insertChannel(newChannel);
User savedUser = userRepository.insertUser(newUser);
// then
Channel foundChaneel = channelRepository.selectChannel(savedChannel.getId());
assert foundChaneel.getUserChannels().stream()
.map(UserChannel::getChannel)
.map(Channel::getName)
.anyMatch(name -> name.equals(newChannel.getName()));
}
}
@ElementCollection
, @ManyToMany
, @OneToMany
, @ManyToOne
, @OneToOne