다중성 : 관계의 종류(eg. 일대다, 다대다..)
단방향, 양방향
연관관계의 주인
결과적으로 말하면 '다'쪽 테이블이 연관관계의 주인이 되어야한다.
만약 '일'쪽 테이블이 주인이 된다면 '다'쪽 테이블을 같은 '일'테이블에 넣어주기위해 같은 객체를 여러번 insert해줘야하게 되기에, 무조건 '다'쪽이 연관관계 주인이 되어야한다.
fk를 통해 join할수있게되니, 해당 컬럼을 통해 join하겠다는 의미로 @JoinColumn 어노테이션을 통해 fk를 지정해줄수 있다.
@ManyToOne @JoinColumn(name = "TEAM_ID") private Team team;
보통 주인이 아닌 객체에서 주인을 너무 자주 호출하거나 필요로 할때 양방향으로 만들어주는데, 주인이 아니면 조회만 가능하다.
주인이 있는 쪽(다)에 @JoinColumn으로 외래키인것을 표시해주고, 주인이 아닌쪽에 mappedBy옵션을 통해 fk에 매핑하겠다는 의미를 준다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
------------------------------------------
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
일이 연관관계 주인인 관계이다.
그런데 방금 '다'쪽이 연관관계의 주인이 되어야한다.물론 객체에서 다음과 같은 경우가 나올수있지만 전에 말한것처럼 DB입장에서는 무조건 다쪽에 FK가 들어가야한다. 따라서 DB에서는 객체와 달리 '다' 테이블에 FK가 들어가게 된다.
일대다 관계를 그린 그림을 보면 당연히 멤버에 외래키(다)가 들어가게됨. 그러면 team객체를 바꿨을때 team테이블이 아닌 member테이블의 값을 변경해줘야함. 즉, 객체와 데이터베이스가 fk를 관리하는 주체가 달라지는것을 볼수 있다.
코드를 보면 이전과 달리 @JoinColumn을 '일'쪽에 넣어주는 것을 볼수 있다. @JoinColumn을 넣지 않으면 중간테이블이 자동으로 만들어지기 때문에 꼭 넣어줘야한다.
@OneToMany()
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
이렇게하여 em.persist()를 해보면 위의 그림과 같이 Team객체가 fk를 가지고 있지만 데이터베이스상에서는 member테이블에 fk가 가있는것을 볼수 있다.
쿼리를 보면
/* create one-to-many row hellojpa.Team.members */ update Member
라고 다른 테이블에 update쿼리가 나간것을 볼수 있다. 사실 성능상의 문제는 크지 않지만 심각한것은 개발을 진행하며 테이블이 많아지고, 코드가 많아진다면 운영이 힘들어진다는 문제가 있다.
그래서 그냥 객체지향적으로 조금 손해(멤버객체는 팀이 필요없지만 팀 필드를 만드는것)가 있더라도 다대일 양방향으로 만드는 것이 편리하다
그냥 다대일 양방향 매핑을 이용하자!
멤버테이블에 TEAM객체 참조를 넣고 @JoinColumn을 붙히고 insertable과 updatable 옵션을 넣으면 읽기 전용이된다.
@ManyToOne
@JoinColumn(name = "TEAM_ID",insertable = false, updatable = false)
private Team team;
해당 옵션을 통해 값이 넣어지지도, 업데이트 되지도 않아 강제적으로 조회만 가능한 양방향 매핑을 완성해주었다.
하지만 일대다 양방향은 사실 공식적으로 지원되진 않으니, 그냥 다대일 양방향 이용하자..ㅎ
일대일 관계는 사실 반대로해도 일대일 매핑이된다. 즉, 주 테이블이나 대상 테이블 중에 외래키를 아무곳이나 넣을수 있다!! 연관관계의 주인을 정했다면, 외래키에 데이터베이스 유니크 제약조건이 추가관리되어야한다!!(안해도 되는데 대신 관리를 엄청 잘해야한다.)
단방향
멤버가 주테이블이다.
다대일 단방향과 유사하다.
양방향 또한 다대일 양방향과 같다보면된다.
단방향
단방향으로는 해당 관계는 지원되지 않는다.
양방향
단방향과 달리 양방향일때는 대상테이블의 참조를 그대로 테이블에 넣으면 된다. 하지만 사실 이것은 주테이블에 양방향의 방향만 바꾼것 뿐이다. 즉 일대일은 내것만 관리할수 있다.
테이블만을 생각할때 어떻게든 일대일 관계는 유효하지만 위의 그림에서 사물함에 fk가 있는것이 유연하게 테이블을 관리할수 있다. 반대로 멤버에 fk가 있다면 테이블 필드를 추가하고 변경해야하는 부분이 많다.(보통 1명이 여러개의 사물함을 갖는 로직으로 변하는 경우가 많기때문에)
하지만 보통 멤버를 많이 조회하기 때문에 백엔드 입장에서는 멤버에 fk가 있는게 성능도 있고 여러 이점이 있다.
일대일 정리
주테이블에 외래키가 있을때
장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인가능
단점 : 값이 없으면 외래키에 null 허용
대상테이블에 외래키가 있을때
장점 : 주테이블과 대상 테이블을 일대일에서 일대다 관계로 변경시 테이블 구조를 유지
단점 : 프록시 기능의 한계로 지연로딩으로 설정해도 항상 즉시 로딩됨.
JPA입장에서 실제 테이블에 데이터가 존재하는지 확인되어야 프록시객체를 만들수 있다.
즉, 멤버 테이블만 조회하는 것이 아닌 락커테이블을 통해 멤버 아이디가 있는지 없는지를 확인해야 그 후에 프록시 객체를 만들거나 null을 만들수있는데, 확인과정에서 어차피 쿼리가 나가게 되기 때문에 굳이 지연로딩을 지원할 필요가 없어진다.
결론부터 말하면 다대다는 사용하면 안된다.
테이블에서 다대다는 연결테이블을 추가해서 일대다, 다대일 관계로 풀어내야한다.
객체는 컬렉션을 사용해서 객체 2개로 다대다가 가능하지만 한계가 분명하다.
다대다관계의 경우 중간 테이블이 하나 새로 만들어진다.
자동으로 만들어 지기때문에 편해보이지만 연결테이블이 단순히 연결만 하고 끝나느것이 아닌 주문시간, 수량과 같은 여러 데이터가 들어올수 있는데, 그것을 커스텀하는것이 불가능하다.
또한 중간테이블을 개발자가 컨트롤할수 없어 쿼리또한 컨트롤이 불가하다는 단점이 있다.
개발자가 직접 연결테이블(중간테이블)용 엔티티를 추가한다. 중간 테이블을 만들고, 일대다, 다대일 관계를 통해 중간테이블과 테이블을 연결하여 사용하는것을 권장. 또한 중간테이블을 엔티티로써 직접 만들기 때문에 새로운 필드들을 추가하여 사용할수 있게 된다.
또한 fk두개를 엮어서 pk로 사용하는 경우가 있지만, 종속된 값이 아닌 관련없는 새로운 id를 만들어 pk로 사용하는것이 바람직하다