JPA part 6
다양한 연관관계 다중성
ManyToOne 다대일
다대일의 대응 관계는 항상 일대다이며, 항상 다쪽이 연관관계의 주인이 된다. 앞서 살펴본 회원, 팀 관계가 다대일 관계이다. 아래를 보면 외래키를 가지고 있는 Member Entity가 주인으로 설정되어 있는 것을 확인할 수 있다.
Member Entity
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
public Member(String id, String username) {
this.id = id;
this.username = username;
}
public void setTeam(Team team) {
this.team=team;
//team 의 members에 멤버 추가
if(!team.getMembers().contains(this)){
team.getMembers().add(this)
}
}
}
Team Entity
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
//추가
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
public Team(String id, String name) {
this.id = id;
this.name = name;
}
public void addMember(Member member){
this.members.add(member);
//멤버에 팀이 안 정해져있으면 팀으로 설정한다.
if(member.getTeam()!=this){
member.setTeam(this);
}
}
public List<Member> getMembers() {
return members;
}
}
일대다 OneToMany
엔티티 하나 이상을 참조하기 때문에 Collection 형태로 객체를 저장할 수 있도록 해야한다. 예제의 팀 객체가 이에 해당한다.
일대다 단방향
팀은 회원을 참조하지만, 회원이 팀을 참조 하지 않는 경우 일대다 단방향이라고 할 수 있다.
위의 연관관계 그림을 보게 되면 특이한 관계 형성이 이루어지는 데, 이는 외래키가 항상 다쪽 테이블에 존재하기 때문에 그렇다.
따라서, 매핑을 팀에서 멤버 쪽으로 해줘야한다.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
//추가
@OneToMany
@JoinColumn(name="TEAM_ID") //--> Member 의 TEAM_ID를 참조하게 된다.
private List<Member> members = new ArrayList<Member>();
}
이런 연관관계를 형성하면, SQL문의 수정이 여러번 일어나게 된다.
Member member1=new Member("member1");
Member member2=new Member("member2");
Team team1=new Team("team1");
//Team 쪽에서 member를 관리하기 때문에, 아래의 코드가 필요하다.
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(member1);
em.persist(member2);
em.persist(team1);
INSERT INTO MEMBER(MEMBER_ID) VALUES("member1");
INSERT INTO MEMBER(MEMBER_ID) VALUES("member2");
INSERT INTO TEAM(TEAM_ID) VALUES("team1");
UPDATE MEMBER SET TEAM_ID="team1" WHERE MEMBER_ID="member1";
UPDATE MEMBER SET TEAM_ID="team1" WHERE MEMBER_ID="member2";
위와 같이 멤버 변수를 처음 INSERT 할때는 팀의 정보가 없기 때문에, 나중에 팀이 입력되면 UPDATE을 통해 팀 정보를 입력되는 것을 확인할 수 있다.
외래키가 외부 테이블에 있다는 점과, 위의 성능 이슈로 인해 일대다 단방향보다는 다대일 양방향을 통한 관리가 더 좋다.
일대다 양방향
사실, 일대다 양방향라는 매핑은 원래는 없지만 억지로 만들수는 있다. 멤버에서 팀 방향으로 읽기만 가능한 연관관계를 추가해주면 된다.
Member Entity
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
//멤버쪽에서는 팀쪽으로 외래키에 대한 수정을 할 수 없도록 지정한다.
@JoinColumn(name="TEAM_ID",insertable=false,updatable=false)
private Team team;
public Member(String id, String username) {
this.id = id;
this.username = username;
}
public void setTeam(Team team) {
this.team=team;
//team 의 members에 멤버 추가
if(!team.getMembers().contains(this)){
team.getMembers().add(this)
}
}
}
일대일 OneToOne
일대일은 서로 관계에 참여 하는 엔티티가 각각 1개일때 생성되는 연관관계이다.
일대일 관계에서는 외래키가 주 테이블에 있을 수 있고, 대상 테이블에도 있을 수 있다. 참조하는 방향을 고려해서 외래키를 어디에 둘지 설정한다.
주테이블에 외래키
주테이블에 외래키를 둬서 주테이블에서 대상테이블로 매핑될 수 있도록 한다.
단방향
멤버는 사물함 하나에만, 사물함 하나는 멤버 하나에만 할당되야하므로, Member 쪽의 LOCKER_ID가 Unique 속성인것을 확인할 수 있다.
Member Entity
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
}
Locker Entity
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String name;
}
위를 보면 주 테이블인 Member Entity에 외래키가 있고, 주테이블에서 연관관계 매핑이 이루어지는 것을 확인 할 수 있다.
양방향
양방향 매핑을 하기 위해서는 대상 테이블인 LOCKER에서 멤버 객체 추가 및, MappedBy 속성만 추가해주면 된다.
Member Entity
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
}
Locker Entity
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy="locker")
private Member member
}
대상 테이블에 외래키
이번에는 대상 테이블에 외래키가 있는 경우에 대해 알아보자
단방향
JPA 에서는 대상 테이블에 외래키가 있는 단방향 관계를 지원하지 않는다.(일대다 단방향에서만 예외적으로 허용한다.) 따라서 이를 이용한 매핑 설계는 할 수 없다. 그래서 이럴때는, 단방향 관계를 Locker에서 Member로 방향을 수정하건나, 양방향 연관관계에서 연관관계 주인을 Locker로 설정하는 방법이 있다. 여기서는 양방향 관계를 이용한 매핑을 수행해보겠다.
양방향
이번에는 연관관계의 주인이 Locker가 되도록 설정해야한다.
Member Entity
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy="member")
private Locker locker;
}
Locker Entity
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String name;
@JoinColumn(name="MEMBER_ID")
private Member member
}
ManyToMany
RDBMS에서 다대다 관계를 표현할 수 없다. 이를 나타내기 위해 중간에 JOIN 테이블을 생성해서 ManyToOne 과 OneToMany 관계를 풀어서 생각해야한다.
위와 같이 멤버와 상품 간에는 다대다 관계가 성립하는데, 이를 표현하기 위해 중간에 조인 테이블을 이용해서 관계를 나타내고 있다.
단방향
Member Entity
@Entity
public class Member{
@Id
@Column(name="MEMBER_ID")
private Long id;
private String username;
@ManyToMany
@JoinTable(name="MEMBER_PRODUCT",joinColumns=@JoinColumn(name="MEMBER_ID"),inverseJoinColumns=@JoinColumn(name="PRODUCT_ID"))
private List<Products> products=new ArrayList<>();
}
@JoinTable annotation을 이용해서 조인 테이블을 명시해준다.
joinColumns에는 회원과 조인테이블로의 조인 정보를 명시해주고
inverseJoinColumns에서는 상품과 조인 테이블로의 조인 정보를 설정해준다.
Product Entity
@Entity
public class Product{
@Id
@Column(name="PRODUCT_ID")
private String id;
private String name;
}
Examples
Product product=new Product();
product.setId("product1");
product.setName("상품1");
em.persist(product);
Member member=new Member();
member.setId("member1");
member.setUsername("회원1");
member.getProducts().add(product);
em.persist(member);
위와 같이 관계를 설정해서 저장하게 되면 아래와 같이 조회를 하는 것이 가능하다.
Member member=em.find(Member.class,"member1");
member.getProducts().stream().forEach(
product -> System.out.println("product: " + product.getName())
)
위의 조회를 진행할때 아래의 sql이 실행되어, 조인테이블과 주문 테이블간의 조인을 통해 해당 정보를 찾아낸다.
SELECT *
FROM MEMBER_PRODUCT MP JOIN PRODUCT P ON MP.PRODUCT_ID = P.PRODUCT_ID
WHERE MP.MEMBER_ID="member1"
양방향
반대 방향에 대해 MappedBy 속성을 추가해주면 양방향 관계가 이루어지는 것이다.
Product Entity
@Entity
public class Product{
@Id
@Column(name="PRODUCT_ID")
private String id;
private String name;
@ManyToMany(mappedBy="products")
private List<Member> members;
}
Product Entity에 반대방향에 관한 설정을 추가해준다.
Examples
Product product=new Product();
product.setId("product1");
product.setName("상품1");
em.persist(product);
Member member=new Member();
member.setId("member1");
member.setUsername("회원1");
//반대방향 연관관계 추가
product.getMembers().add(member);
member.getProducts().add(product);
em.persist(member);
위와 같이 관계를 설정해서 저장하게 되면 아래와 같이 조회를 하는 것이 가능하다.
Product product=em.find(Product.class,"product1");
product.getMembers().stream().forEach(
member -> System.out.println("member: " + member.getName())
)
연결 엔티티
실제로는 위와 같이 조인만 존재하지 않는다. 멤버가 상품을 주문하면서 파생되는 정보들도 존재하게 된다. 예를 들어, 주문하 상품의 갯수, 주문 날짜, 등 다양한 컬럼들이 생기게 되는데, @ManyToMany를 이용한 조인 테이블 방식으로는 이를 저장할 수 없다.
이를 해결하기 위해 두 개의 엔티티를 중간에서 연결해주는 연결 엔티티를 생성해서 연결해줘야 한다.
Member Entity
@Entity
public class Member{
@Id
@Column(name="MEMBER_ID")
private String id;
@OneToMany(mappedby="member")
private List<MemberProduct> memberProducts;
}
회원과 회원상품 엔티티간에는 다대일 양방향 연관관계가 형성되며 연관관계의 주인은 회원상품 엔티티에 존재하게 된다.
Product Entity
@Entity
public class Product{
@Id
@Column(name="PRODUCT_ID")
private String id;
private String name;
}
상품에서 어떤 회원들이 해당 상품이 구매하는 지에 대한 탐색을 진행하지 않으면 굳이 양방향 연관관계를 구성하지 않고 다대일 단방향으로 구성해도 된다. 이는 비즈니스 요구사항에 맞춰 판단하면 되는 부분이다.
MemberProduct Entity
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct{
@Id
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@Id
@ManyToOne
@JoinColun(name="PRODUCT_ID")
private Product product;
private int orderAmount;
}
MemberProductId class
public class MemberProductId implements Serializable{
private String member; //MemberProduct의 member
private String product //MemberProduct의 product
@Override
public boolean equals(Object o){
}
@Override
public int hashCode(){
}
}
MemberProduct 에서는 member 와 product를 이용해서 복합 기본키를 구성하도록 하였다.
복합 키 구성을 위해 MemberProductId라는 별도의 식별자 클래스를 활용하고 @IdClass를 이용해서 식별자 클래스를 명시한다.
Example
//회원 저장
Member member=new Member();
member.setId("member1");
member.setUsername("회원1");
em.persist(member)
//상품 저장
Product product=new Product();
product.setId("product1");
product.setName("상품1");
em.persist(product);
//회원 상품 저장
MemberProduct memberProduct=new MemberProduct();
memberProduct.setMember(member);
memberProduct.setProduct(product);
memberProduct.setOrderAmount(2);
em.persist(memberProduct);
아래와 같이 회원 상품 엔티티에 대한 조회를 수행할 수 있다.
MemberProductId memberProductId=new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("product1");
MemberProduct memberProduct=em.find(MemberProduct.class,memberProductId);
Member member=memberProduct.getMember();
Product product=memberProduct.getProduct();
System.out.println("Member: " + member.getUsername());
System.out.println("Product: " + product.getName());
System.out.println("Order Amount: " + memberProduct.getOrderAmount());
MemberProduct 객체는 식별자 클래스를 이용해서 복합 기본키로 설정되어 있다. 따라서 조회를 할때에도 식별자 클래스를 이용해서 객체를 조회를 할 수 있다.
연결 엔티티에 대리키 방식 추가
위의 예제에서는 복합 기본키를 사용하기 위해 식별자 클래스릉 이용해서 처리하였다. 하지만 이렇게 하려면, 식별자 클래스를 만들어줘야 되고, @IdClass을 이용해서 식별자 클래스를 명시해주고, 매번 조회를 할떄 마다 식별자 클래스를 활용해야하는 불편한 점이 있었다. 이를 개선하기 위해 DB에서 제공하는 대리키 방식을 이용한다.
위의 연결 엔티티는 ORDERS_ID 라는 기본키를 가지고 있으며, MEMBER_ID 나 PRODUCT_ID는 Foreign Key로 유지하고 있다.
Order Entity
@Entity
public class Order{
@Id
@GeneratedValue
@Column(name="ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@ManyToOne
@JoinColun(name="PRODUCT_ID")
private Product product;
private int orderAmount;
}
Order Entity에 기본키만 새로 추가 하고 나머지 엔티티들에는 변경사항이 없다.
Example
//회원 저장
Member member=new Member();
member.setId("member1");
member.setUsername("회원1");
em.persist(member)
//상품 저장
Product product=new Product();
product.setId("product1");
product.setName("상품1");
em.persist(product);
//회원 상품 저장
Order order=new Order();
order.setMember(member);
order.setProduct(product);
order.setOrderAmount(2);
em.persist(order);
아래와 같이 회원 상품 엔티티에 대한 조회를 수행할 수 있다.
//기본키를 이용해서 주문 객체를 찾을 수 있다.
Order order=em.find(Order.class,1L);
Member member=order.getMember();
Product product=order.getProduct();
System.out.println("Member: " + member.getUsername());
System.out.println("Product: " + product.getName());
System.out.println("Order Amount: " + order.getOrderAmount());
DB에서 생성해주는 대리키를 기본키로 설정하여, 주문 객체를 조회하는 것을 간편화 시킬 수 있다.
References
book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저
댓글남기기