-
양방향 @ManyToMany 관계를 갖는 모델을 정의하고 데이터 가져오기Today I Learned 2022. 10. 29. 22:51
어제 도장에서 결국 모델 정의를 마무리하지 못하고 집에 돌아왔다. 이렇게 줄이고 줄인 최소한의 모델에서도 도대체 어느 부분이 어려웠다는 말인가?
게시글 목록과 상세 게시글을 구현하기 위한 데이터를 가져오기 위해 왼쪽 사진과 같이 백엔드 서버에서 가져와야 하는 데이터들을 구성했다. 이때 게시글과 사용자의 관계는 게시글에 여러 명의 사용자가 포함이 될 수 있고, 반대로 사용자 역시 자신이 참가하는 여러 개의 게시글을 알 수 있어야 하는 다대 다로 서로를 양방향으로 알 수 있어야 하는 구조였다. 이 구조를 지금 알고 있던 내용만으로는 백엔드에서 정의하기가 쉽지 않았다.
관계형 데이터베이스에서 테이블 간의 관계를 나타내기 위한 OneToOne, OneToMany, ManyToOne, ManyToMany 관계 중 ManyToMany 관계를 적용해 데이터베이스 모델을 만들어보는 실험을 진행해보기로 했다.
두 테이블이 서로 관계를 맺기 위해서는 데이터를 사용하는 관점에서 주도권을 갖는 부모 테이블의 요소가 갖는 Key(Primary Key)를 자식 테이블에 Foreign Key로 부여해 서로 연결하는 방식으로 관계를 지정한다.
JPA에서는 일대 다 관계를 표현하기 위해 Entity 객체의 멤버 변수로 다른 Entity 객체의 컬렉션을 선언하고 @OneToMany 어노테이션을, 다대 일 관계에서는 다른 Entity 객체 하나를 선언하고 @ManyToOne 어노테이션을 붙이고 @JoinColumn 어노테이션으로 Key를 연결한다.
양방향 관계에서는 @OneToMany Entity 객체 멤버 변수의 어노테이션에 (mappedBy = "@JoinColumn이 붙은 Entity 객체 멤버 변수명")을 부여한다.
예시는 다음의 소스코드와 같다.
// 단방향 일대 다 관계 @Entity public class Parent { @Id @GeneratedValue private Long parentId; @OneToMany @JoinColumn(name = "PARENT_ID") private List<Child> childList; } @Entity public class Child { @Id @GeneratedValue private Long childId; }
// 단방향 다대 일 관계 @Entity public class Parent { @Id @GeneratedValue private Long parentId; } @Entity public class Child { @Id @GeneratedValue private Long childId; @ManyToOne @JoinColumn(name = "PARENT_ID") private Parent parent; }
// 양방향 일대 다, 다대 일 관계 @Entity public class Parent { @Id @GeneratedValue private Long parentId; @OneToMany(mappedBy = "parent") private List<Child> childList; } @Entity public class Child { @Id @GeneratedValue private Long childId; @ManyToOne @JoinColumn(name = "PARENT_ID") private Parent parent; }
나의 경우에는 다대 다 양방향 관계를 표현해야 했는데, 테이블 내 한 요소의 Key Column에 두 개 이상의 값을 부여할 수 없기 때문에 두 개의 테이블만으로는 테이블에 중복되는 요소를 추가하지 않고서는 표현하는 것이 불가능했다. 따라서 두 테이블의 Key를 이용해 새로운 테이블을 구성해야 했다.
다음과 같이 한쪽에서 반대쪽의 Entity 객체를 나타낸 멤버 변수에 @JoinTable 어노테이션을 이용해 데이터베이스 내에서 새로운 테이블을 생성하도록 했다. 어노테이션 내에서는 테이블의 이름과, 연결할 Column들을 @JoinColumn 어노테이션으로 지정했다.
// Post.java @Entity public class Post { @Id @GeneratedValue private Long postId; private String detail; @ManyToMany @JoinTable( name = "POST_PERSON", joinColumns = @JoinColumn(name = "POST_ID"), inverseJoinColumns = @JoinColumn(name = "PERSON_ID")) private List<User> participants; // Constructors, Methods, ... } // Entity.java @Entity @Table(name = "PERSON") public class User { @Id @GeneratedValue private Long personId; private String name; @ManyToMany(mappedBy = "participants") private List<Post> participatingExercises; // Constructors, Methods, ... }
데이터베이스 환경을 구축하고 백엔드 서버를 실행했을 때 H2 Console을 통해 확인한 테이블 구조는 다음과 같다.
예시 데이터를 삽입하고 결과를 확인하기 위해 구축한 백엔드 구조는 다음과 같다.
// Controller @RestController @RequestMapping("/test") public class TestController { private final TestService testService; public TestController(TestService testService) { this.testService = testService; } @GetMapping("/posts") public void posts() { List<Post> posts = testService.findAllPosts(); System.out.println(posts); } @GetMapping("/users") public void users() { List<User> users = testService.findAllUsers(); System.out.println(users); } @GetMapping("/posts/user/{userId}") public void postsByUserId( @PathVariable Long userId ) { List<Post> postsByUserId = testService.findPostsByUserId(userId); System.out.println(postsByUserId); } @GetMapping("/users/post/{postId}") public void usersByPostId( @PathVariable Long postId ) { List<User> usersByPostId = testService.findUsersByPostId(postId); System.out.println(usersByPostId); } }
// Service @Service @Transactional public class TestService { private final PostRepository postRepository; private final UserRepository userRepository; public TestService(PostRepository postRepository, UserRepository userRepository) { this.postRepository = postRepository; this.userRepository = userRepository; } public List<Post> findAllPosts() { return postRepository.findAll(); } public List<User> findAllUsers() { return userRepository.findAll(); } public List<Post> findPostsByUserId(Long userId) { User user = userRepository.findByPersonId(userId); return postRepository.findByParticipants(user); } public List<User> findUsersByPostId(Long postId) { Post post = postRepository.findByPostId(postId); return userRepository.findByParticipatingExercises(post); } }
// Repositories public interface PostRepository extends JpaRepository<Post, Long> { Post findByPostId(Long postId); List<Post> findByParticipants(User user); } public interface UserRepository extends JpaRepository<User, Long> { User findByPersonId(Long userId); List<User> findByParticipatingExercises(Post post); }
한 쪽 Repository에서 각 테이블의 Key를 이용해 생성된 테이블(예시에서는 POST_PERSON 테이블)로 연결된 데이터를 참조하기 위해서는 JPA 인터페이스 명칭에 @ManyToMany 어노테이션이 붙은 멤버 변수 이름(예시에서는 Participants나 ParticipatingExercises)을 사용해야 하는 점을 유의할 수 있을 것이다.
실험 데이터와 Controller에서 postsByUserId, usersByPostId를 실행시킨 결과는 다음과 같다.
1번 사용자가 포함된 게시글 조회
4번 사용자가 포함된 게시글 조회
2번 게시글에 포함된 사용자 조회
3번 게시글에 포함된 사용자 조회
Process
https://fuschia-impala-bd5.notion.site/JPA-a4670c1e8c5f411ab02cd07c6e17e60e
'Today I Learned' 카테고리의 다른 글
POST 요청으로 연관관계를 갖는 객체들의 데이터 생성하기 (0) 2022.11.01 모델 구조 설계는 데이터베이스 설계가 아닌 객체 설계 먼저 (0) 2022.10.30 너무 큰 작업은 task를 쪼개 MVP로 만들기 (0) 2022.10.28 2주차 목요일 데일리 스프린트 작업 회고 (0) 2022.10.27 첫 스프린트 티켓 끊기 (0) 2022.10.26