다양한 연관관계 다중성

ManyToOne 다대일

relationship_owner

다대일의 대응 관계는 항상 일대다이며, 항상 다쪽이 연관관계의 주인이 된다. 앞서 살펴본 회원, 팀 관계가 다대일 관계이다. 아래를 보면 외래키를 가지고 있는 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 형태로 객체를 저장할 수 있도록 해야한다. 예제의 팀 객체가 이에 해당한다.

일대다 단방향

팀은 회원을 참조하지만, 회원이 팀을 참조 하지 않는 경우 일대다 단방향이라고 할 수 있다.

one_to_Many_uni

위의 연관관계 그림을 보게 되면 특이한 관계 형성이 이루어지는 데, 이는 외래키가 항상 다쪽 테이블에 존재하기 때문에 그렇다.

따라서, 매핑을 팀에서 멤버 쪽으로 해줘야한다.

@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을 통해 팀 정보를 입력되는 것을 확인할 수 있다.

외래키가 외부 테이블에 있다는 점과, 위의 성능 이슈로 인해 일대다 단방향보다는 다대일 양방향을 통한 관리가 더 좋다.

일대다 양방향

사실, 일대다 양방향라는 매핑은 원래는 없지만 억지로 만들수는 있다. 멤버에서 팀 방향으로 읽기만 가능한 연관관계를 추가해주면 된다.

one_to_Many_bi

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개일때 생성되는 연관관계이다.

일대일 관계에서는 외래키가 주 테이블에 있을 수 있고, 대상 테이블에도 있을 수 있다. 참조하는 방향을 고려해서 외래키를 어디에 둘지 설정한다.

주테이블에 외래키

주테이블에 외래키를 둬서 주테이블에서 대상테이블로 매핑될 수 있도록 한다.

단방향

onetoone_prime_uni

멤버는 사물함 하나에만, 사물함 하나는 멤버 하나에만 할당되야하므로, 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에 외래키가 있고, 주테이블에서 연관관계 매핑이 이루어지는 것을 확인 할 수 있다.

양방향

onetoone_prime_bi

양방향 매핑을 하기 위해서는 대상 테이블인 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
}

대상 테이블에 외래키

이번에는 대상 테이블에 외래키가 있는 경우에 대해 알아보자

단방향

oneToone_sub_uni.png

JPA 에서는 대상 테이블에 외래키가 있는 단방향 관계를 지원하지 않는다.(일대다 단방향에서만 예외적으로 허용한다.) 따라서 이를 이용한 매핑 설계는 할 수 없다. 그래서 이럴때는, 단방향 관계를 Locker에서 Member로 방향을 수정하건나, 양방향 연관관계에서 연관관계 주인을 Locker로 설정하는 방법이 있다. 여기서는 양방향 관계를 이용한 매핑을 수행해보겠다.

양방향

oneToone_sub_bi.png

이번에는 연관관계의 주인이 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 관계를 풀어서 생각해야한다.

manytomany_relationship

위와 같이 멤버와 상품 간에는 다대다 관계가 성립하는데, 이를 표현하기 위해 중간에 조인 테이블을 이용해서 관계를 나타내고 있다.

단방향

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_limitation

실제로는 위와 같이 조인만 존재하지 않는다. 멤버가 상품을 주문하면서 파생되는 정보들도 존재하게 된다. 예를 들어, 주문하 상품의 갯수, 주문 날짜, 등 다양한 컬럼들이 생기게 되는데, @ManyToMany를 이용한 조인 테이블 방식으로는 이를 저장할 수 없다.

Connetiction_Entity

이를 해결하기 위해 두 개의 엔티티를 중간에서 연결해주는 연결 엔티티를 생성해서 연결해줘야 한다.

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에서 제공하는 대리키 방식을 이용한다.

artificial_key

위의 연결 엔티티는 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 프로그래밍 -김영한 저

book_link

태그: ,

카테고리:

업데이트:

댓글남기기