본문으로 바로가기

다양한 연관관계 매핑 정리

category SPRING/JPA 2022. 3. 17. 16:41
728x90
반응형
SMALL

연관관계

다대일(회원-팀)

회원

public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name="TEAM_ID") // @JoinColumn: 외래키 매핑시 사용, 생략 가능, 생략시 기본전략-> 필드명(team)_참조하는테이블컬럼명(TEAM_ID)=>team_TEAM_ID
    private Team team;

    public void setTeam(Team team) {
        this.team = team;
    }
}

public class Team {

    @Id
    @Column(name="TEAM_ID")
    private String id;

    private String name;
}

테스트

 "select m from Member m join m.team t where "+
                "t.name= :teamName";

양방향(팀-회원)

일대다 는 여러 건과 관계를 맺을 수 있으므로 컬렉션을 사용해야 함.

member

public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name="TEAM_ID") // @JoinColumn: 외래키 매핑시 사용, 생략 가능, 생략시 기본전략-> 필드명(team)_참조하는테이블컬럼명(TEAM_ID)=>team_TEAM_ID
    private Team team;

    public void setTeam(Team team) {
        this.team = team;
    }
}

team

public class Team {

    @Id
    @Column(name="TEAM_ID")
    private String id;

    private String name;

    @OneToMany(mappedBy="team") // @mappedBy: 양방향 매핑일때 사용, 반대쪽 매핑의 필드이름을 값으로 주면됨, 현재는 member이므로 team을 줬음
    private List<Member> members = new ArrayList();

}

일대다 조회

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();

@mappedBy 사용이유?

객체엔 양방향 연관관계가 없음, 양방향으로 보이게만 하는것일뿐

  • 연관관계의 주인은 mappedBy 속성을 사용하지 않는다
  • 주인이 아니면 읽기만 할 수 있다.
public class Team {

    @Id
    @Column(name="TEAM_ID")
    private String id;

    private String name;

    @OneToMany(mappedBy="team") 
    private List<Member> members = new ArrayList();

}

여기서의 주인은 Member 엔티티인 것을 알 수 있는 대목이다.

양방향 주의점

회원에서 팀에게 가입할때

Team team1 = new Team("team1", "팀1");
em.persist(team1);

Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 양방향
em.persist(member1);

Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 양방향
em.persist(member2);

여기 구문엔 버그가 존재한다.

member1.setTeam(teamA); // 양방향
member2.setTeam(teamB); // 양방향
Member findMember = TeamA.getMember(); // member1이 여전히 조회됨

TeamA에서 TeamB로 변경할때 TeamA와의 관계를 제거를 하지 않았기 때문

Member엔티티의 해당 메서드를 바꿔줘야한다.

public void setTeam(Team team) {
        this.team = team;
}

이렇게 바꿔주자

public void setTeam(Team team) {
    //기존 팀과 관계 제거
    if(this.team != null){
        this.team.getMembers().remove(this);
    }
    this.team = team; //회원에서 팀에게 가입
    team.getMembers().add(this); 팀에서 회원을 자기팀으로 가입
}

이렇게 양방향의 장점은 반대방향으로 객체 그래프 탐색이 가능하다는 점이지만 연관관계의 주인 설정과 로직 관리도 잘해야 한다.

관계 설계 하기

Member

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;

    private String username;

    private String city;
    private String street;
    private String zipcode;


    @OneToMany(mappedBy = "member")
    private List<Order>orders = new ArrayList<>();

}

Orders

@Entity
@Table(name="ORDERS")
@Getter
@Setter
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="ORDER_ID")
    private Long id;

    @ManyToOne
    @JoinColumn
    private Member member;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList();

    @Temporal(TemporalType.TIMESTAMP)
    private Date orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문상태

    //연관관계 메소드
    public void setMember(Member member) {
        if(this.member!=null) {
            this.member.getOrders().remove(this);
        }
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);

    }

    public enum OrderStatus{
        ORDER, CANCEL
    }
}

Order_Item

@Entity
@Table(name="ORDER_ITEM")
@Getter
@Setter
public class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="ORDER_ITEM_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name="ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name="ITEM_ID")
    private Item item;

    private int orderPrice;

    private int count;
}

Item

@Entity
@Table(name="ITEM")
@Getter
@Setter
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity; 
}

Member 와 Orders 연관관계 등록시

Member member = new Member();
Order order = new Order();
order.setMember(member); // member -> order, order-> member 양방향 매핑
객체 그래프 사용

주문한 회원 탐색

Order order = em.find(Order.class, orderId);
Member member = order.getMember(); // 주문한 회원, 참조 사용

주문한 상품 하나 탐색

Order order = em.find(Order.class, orderId);
orderItem = order.getOrderItems.get(0);
Item = orderItem.getItem();

실무에서는 다대일 또는 일대다 관계를 가장 많이 사용하고 다대다는 거의 사용하지 않음

다대일

외래키는 항상 다 쪽에 있다.

다대일(단방향)

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private String id;

    private String username;

    //다대일 단방향 관계
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;

}

Team

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;

    private String name;

}

회원은 Member.team으로 팀 엔티티를 조회할 수 있지만 반대로 팀에는 회원을 참조하는 필드가 없다.

다대일(양방향) 1:N N:1

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private String id;

    private String username;

    //다대일 양방향 관계
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;

    public void setTeam(Team team){
        this.team = team;

        //무한 루프 방지
        if(!team.getMembers().contains(this)){
            this.getMembers().add(this);        }
    }

}

Team

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;

    private String name;

    //다대일 양방향 관계
    @OneToMany(mappedBy = "team")
    private List<Member>members = new ArrayList<Member>();

    public void addMember(Member member){
        this.members.add(member);
        //무한루프 방지
        if(member.getTeam() != this){
            member.setTeam(this);
        }
    }
}

연관관계의 주인이 아닌 Team.members 는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용한다.

양방향에서는 항상 서로를 참조하게 해야하며 이런 편의 메소드 Member 엔티티의 setTeam() 그리고 Team 엔티티의 addMember() 메소드가 이런 편의 메소드이다.

꼭! 무한 루프에 빠지지 않는 로직을 작성하도록 하자

일대다

일대다(단방향)

하나의 팀은 여러 회원을 조회할 수 있다.

그러나 회원은 팀을 조회할 수 없다.

특이한 것은 Member 엔티티가 Team의 외래키를 관리한다.

그러나 Member엔티티에서는 외래 키를 매핑할 수 있는 참조필드가 없다.

Team

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;

    private String name;

    //일대다 단방향 관계
    @OneToMany
    @JoinColumn(name="TEAM_ID") // Member 테이블의 TEAM_ID
    private List<Member>members = new ArrayList<Member>();
}

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private String id;

    private String username;

    //Team을 참조할 수 있는 필드가 없어
}

일대다 단방향 관계를 매핑할 땐 @JoinColumn을 명시해야 한다.

일대다 단방향 관계 단점

매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 점이다.

본인 테이블에 외래키가 있으면 Insert sql을 한번에 끝낼수 있으나 다른 테이블에 외래키가 있으면 연관관계 처리를 위해 update sql을 추가로 실행해줘야 한다.

Team 엔티티는 Member 를 알고있지만 Member 엔티티는 Team을 모른다.

public void insertTest(){
    Member member1 = new Member("Member1");
    Member member2 = new Member("Member2");

    Team team1 = new Team("team1");
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);

    em.persist(member1); //insert member1
    em.persist(member2); //insert member2
    em.persist(team1); //insert team1, update member1.fk, update member2.fk
}

성능상 문제도 있으며 관리도 까다롭기 때문에 일대다 단방향 보단 다대일 양방향 매핑이 권장된다.

 

일대다(양방향)

다대일 양방향과 같은말이다.

정확히 @OneToMany 는 연관관계의 주인이 될수 없다. 항상 다 쪽이 주인이며 @ManyToOne 에는 mappedBy 속성이 없음

그래서 제한이 있지만 일대다 단방향 매핑 반대편의 다대일 단방향 매핑을 읽기 전용으로 서정하면 된다.

Team

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;

    private String name;

    //일대다 양방향 관계
    @OneToMany
    @JoinColumn(name="TEAM_ID") 
    private List<Member>members = new ArrayList<Member>();
}

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;
    private String username;

    //읽기만 가능해게 적용 일대다 양방향 관계
    @ManyToOne
    @JoinColumn(name="TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

@JoinColumn(name="TEAM_ID", insertable = false, updatable = false)

이런식으로 읽기전용으로만 설정해두었지만

결국 일대다 단방향 매핑이 가지는 단점을 그대로 가진다. 될 수 있으면 다대일 양방향 매핑이 권장된다.

일대일

일대일 관계는 주 테이블이나 대상테이블 둘 중 한곳에 외래키를 가질 수 있다.

  • 주테이블에 외래키
  • 장점은 주 테이블이 외래키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과의 연관관계 확인 가능
  • 대상테이블에 외래키
  • 장점은 테이블 관계를 일대일 에서 일대다로 변경시 테이블 구조를 그대로 유지 가능

​ 대상 테이블에 외래키가 있는 매핑은 일대다 단방향 관계에서는 허용하지만 일대일 단방향은 허용되지 않는 다. 때문에 양방향으로 설정해야한다.

주 테이블 외래키 단방향

Member

@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
public class Locker {

    @Id @GeneratedValue
    @Column(name="LOCKER_ID")
    private Long id;
    private String name;

}

주 테이블 외래키 양방향

Locker

@Entity
public class Locker {

    @Id @GeneratedValue
    @Column(name="LOCKER_ID")
    private Long id;
    private String name;

    //양방향
    @OneToOne(mappedBy="locker")
    private Member member;
}

대상 테이블 외래키 단방향

일대일 단방향은 에서 대상 테이블 외래키는 허용되지 않으므로 양방향을 이용하자

대상 테이블 외래키 양방향

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;
    private String username;

    //양방향
    @OneToOne(mappedBy="member")
    private Locker locker;
}

Locker

@Entity
public class Locker {

    @Id @GeneratedValue
    @Column(name="LOCKER_ID")
    private Long id;
    private String name;

    //양방향
    @OneToOne(name="MEMBER_ID")
    private Member member;

}

위의 예제에서 Locker.member는 지연로딩이 가능하지만

Member.locker는 지연로딩으로 설정해도 즉시로딩이 된다.

프록시의 한계 때문에 발생하는 문제 -> bytecode instrumentation 을 사용하여 해결이 가능하다는데?

다대다

객체는 테이블과 달리 다대다 관계를 만들 수 있다.

그러나 실무에서 사용하기엔 한계가 있다.

Member_Product는 Member의 기본키와 Product의 기본키를 외래키와 동시에 자신의 기본키로

복합키로서 소지하고 있을 것이다.

그러나 Member_Product 자체로서 가져야 하는 컬럼이 생긴다면 Member 엔티티나 Product 엔티티에서 이들을 매핑할수가 없기 때문에다.

코드를 보고 이해해보자

다대다 단방향

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @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<Product> products = new ArrayList<Product>();
}

@JoinTable.name : 연결 테이블 지정, MEMBER_PRODUCT 테이블이 지정됨

@JoinTable.joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정

@JoinTable.inverseJoinColumns: 반대 방향인 상품과 매핑할 조인 컬럼정보 지정

Product

@Entity
public class Product {

    @Id @GeneratedValue
    @Column(name="PRODUCT_ID")
    private String id;

    private String name;
}

다대다 양방향

Product

@Entity
public class Product {

    @Id
    private String id;

    private String name;
    //역방향
    @ManyToMany(mappedBy="products")
    private List<Member> members;
}

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @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<Product> products = new ArrayList<Product>();

    //양방향 연관관계 편의 메소드 추가
    public void addProduct(Product product){
        products.add(product);
        product.getMembers().add(this);
    }
}

현 코드에서는 MEMBER_PRODUCT 는 @ManyToMany로 인해서 자동으로 생기는 버퍼 테이블이다.

그러나 MEMBER_PRODUCT 에서 추가로 필요한 컬럼이 필요하다면 직접 MEMBER_PRODUCT 엔티티 코드를 생성해야 되기 때문에 @ManyToMany 를 사용할 수 없게 된다.

결국 @OneToMany를 사용하고 MEMBER_PRODUCT 엔티티 코드를 생성하여 다대다를 극복해야한다

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;
    private String username;

    //역방향, MemberProduct이 외래키를 소지
    @OneToMany(mappedBy="member")
    private List<MemberProduct> memberProducts;

}

Product

@Entity
public class Product {

    @Id @Column(name="PRODUCT_ID")
    private String id;

    private String name;
}

Product엔티티에서 MemberProduct 엔티티로 객체 그래프 탐색기능이 필요치 않으므로 연관관계 없음.

MemberProduct

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct  {

    @Id
    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member; //MemberProductId.member와 연결

    @Id
    @ManyToOne
    @JoinColumn(name="PRODUCT_ID")
    private Product product; //MemberProductId.product와 연결

    private int orderMount;
}

MemberProduct 식별자 클래스

public class MemberProductId implements Serializable  {

    private String member;
    privaet String product;

    @Override
    public boolean equals(Object o){..}

    @Override
    public int hashCode(){..}

}

JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 만들어야한다.

@IdClass를 이용하여 식별자 클래스를 지정할 수 있다.

식별자 클래스의 특징

  • 복합 키는 별도의 식별자 클래스를 만들어야함
  • Serializable을 구현
  • equals 와 hashCode 메소드를 구현
  • 기본 생성자 필요
  • 식별자 클래스는 public
  • @IdClass 외에 @EmbeddedId 방법도 존재

저장코드

public void save(){
    //회원 저장
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    em.persist(member1);

    //상품 저장
    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품1");
    em.persist(productA);

    //회원상품 저장
    MemberProduct memberProduct = new MemberProduct();
    memberProduct.setMember(member1);
    memberProduct.setProduct(productA);
    memberProduct.setOrderAmount(2);

    em.persist(memberProduct);
}

조회코드

public void find(){
    MemberProductId memberProductId = new MemberProductId();
    memberProductId.setMember("member1");
    memberProductId.setProduct("product1");

    MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);

    Member member = memberProduct.getMember();
    Product prodict = memberProduct.getProduct();

    System.out.println(member.getUserName());
    System.out.println(product.getName());
    System.out.println(memberProduct.getOrderAmount());
}

이와 같이 복합키를 사용하는 방법은 복잡하다.

단순히 컬럼 하나만 기본 키로 사용하는 것과 비교하여 복합 키 를 사용하면 ORM 매핑에서 처리할 일이 상당히 많아진다.

때문에 추천하는 기본 키 생성전략은 대리키 Long 값을 사용하는 것이다.

간편하고 영구히 쓸수 있으며, ORM 매핑시에 복합 키를 만들지 않아도 된다.

다대다 새로운 기본 키 사용

Order

@Entity
public class Order  {

    @Id @GeneratedValue
    @Column(name="ORDER_ID")
     private Long id;  //대리키 사용

    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member; 

    @ManyToOne
    @JoinColumn(name="PRODUCT_ID")
    private Product product; 

    private int orderMount;
    ...
}

Member

@Entity
public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;
    private String username;

    @OneToMany(mappedBy="member")
    private List<Order> order = new ArrayList();

}

Product

@Entity
public class Member {

    @Id
    @Column(name="PRODUCT_ID")
    private String id;
    private String name;
    ...
}

저장코드

public void save(){
    //회원 저장
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    em.persist(member1);

    //상품 저장
    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품1");
    em.persist(productA);

    //주문 저장
    Order order = new Order();
    order.setMember(member); //주문회원 연관관계
    order.setProduct(productA); //주문상품 연관관계
    order.setOrderAmount(2);

    em.persist(order);
}

조회코드

public void find(){

    Long orderId = 1L;
    Order order = em.find(Order.class, orderId);

    Member member = order.getMember();
    Product product = order.getProduct();

    System.out.println(member.getUserName());
    System.out.println(product.getName());
    System.out.println(order.getOrderAmount());

}

다대다 에서 식별 클래스 또는 새로운 대리키 를 사용하는 시점에서

식별 클래스를 사용할 때의 부모테이블의 기본키를 받아서 자식테이블의 기본키+외래키 조합으로 사용하는 것을 식별관계

단순히 외래키로 사용하는 것이 비식별관계라고 한다.

객체 입장에서는 비식별 관계를 사용하는 것이 단순하고 편리하게 ORM에 매핑할 수 있다.

728x90
반응형
LIST

'SPRING > JPA' 카테고리의 다른 글

객체지향 쿼리언어  (0) 2022.03.26
jpa metamodel must not be empty! 테스트 에러  (0) 2022.03.24
JPA 상에서의 코드성데이터 처리방법중 하나  (0) 2022.03.21
프록시와 연관관계 & 값 타입  (0) 2022.03.21
JPA 시작  (0) 2021.11.30