연관관계 - 양방향

XingXi·2023년 12월 31일
0

JPA

목록 보기
11/23
post-thumbnail

🍕 Reference

자바 ORM 표준 JPA 프로그래밍 : 교보문고
자바 ORM 표준 JPA 프로그래밍 - 기본편 : 인프런

Member ( 1 )

package org.example.javamain.Entity;

import javax.persistence.*;

@Entity
public class Member3
{
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USER_NAME")
    private String userName;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

Team ( N )

@Entity
public class Team
{
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String teamName;

현재 Team 에서는 Member 를 조회할 방법이 없다
데이터베이스의 경우 외래키 를 JOIN 하여
연관관계에 있는 두개의 테이블을 동시에 조회할 수 있다.

select *
from Member m 
JOIN Team t on m.TEAM_ID = t.TEAM_ID
select *
from Team t 
JOIN Member m on m.TEAM_ID = t.TEAM_ID

그러기 위해서 외래키가 없는 Team 객체에 다음과 같은 필드를 추가한다.

❄️ 양방향 Mapping

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

Team 과 Member 의 연관관계는 1 : N 임으로
OneToMany
mappedby 속성을 사용하여 Member3 객체의 어느 필드와 연결이 되어 있는지 나타낸다.

객체에서 사실 양방향보다는 단방향 Mapping이 서로 되어 있는 상태로 보는게 더 바람직하다.

만약 mappedby 속성에 존재하지 않는 필드 이름을 넣으면

    @OneToMany(mappedBy = "team1")
    private List<Member3> members = new ArrayList<>();![]

Exception in thread "main" org.hibernate.AnnotationException: mappedBy reference an unknown target entity property: org.example.javamain.Entity.Member3.team1 in org.example.javamain.Entity.Team.members
	at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:785)
	at org.hibernate.cfg.annotations.CollectionBinder$1.secondPass(CollectionBinder.java:736)
	at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:54)
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1696)
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1664)
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:287)
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:904)
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:935)
	at org.hibernate.jpa.HibernatePersistenceProvider.createEntityManagerFactory(HibernatePersistenceProvider.java:56)
	at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:79)

mappedby 로 타겟이 된 필드를 찾지못하여 예외가 발생하는 것을 볼 수 있다.

양방향 Mapping 시 연관관계 주인인 객체에서 값을 할당해야한다.

main : 연관관계 주인이 아닌 Team이 할당하는 경우

public static void main(String[] args)
    {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em  = emf.createEntityManager();
        EntityTransaction et  = em.getTransaction();
        et.begin();

        try
        {
            Member3 member3 = new Member3();
            member3.setUserName("member3");
            em.persist(member3);

            Team team = new Team();
            team.setTeamName("A TEAM");
            team.getMembers().add(member3);
            em.persist(team);

            et.commit();
        }
        catch (Exception e)
        { et.rollback(); }

        finally
        { em.close(); }

        emf.close();
    }


지금의 경우 양방향 Mapping 이 설정되어 있지만 연관관계의 주인인
Member에서 값을 조작하는 것이 아닌
Team 에서 Member 값을 할당했다.
연관관계에서 연관관계 주인이 아니라면 읽기 전용 상태이기 때문에
할당되지 않는다.

main : 연관관계 주인인 Member 가 값을 할당하는 경우

public static void main(String[] args)
    {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em  = emf.createEntityManager();
        EntityTransaction et  = em.getTransaction();
        et.begin();

        try
        {
            Team team = new Team();
            team.setTeamName("A TEAM");
            em.persist(team);

            Member3 member3 = new Member3();
            member3.setUserName("member3");
            member3.setTeam(team);
            em.persist(member3);

            et.commit();
        }
        catch (Exception e)
        { et.rollback(); }

        finally
        { em.close(); }

        emf.close();
    }

양방향 Mapping 시

양쪽에 값을 할당해주는 것이 좋다.

main : 연관관계 주인인 Member 만 Team 을 할당하는 경우

try
        {
            Team team = new Team();
            team.setTeamName("A TEAM");
            em.persist(team);

            Member3 member3 = new Member3();
            member3.setUserName("member3");
            member3.setTeam(team);
            em.persist(member3);

            Team findTeam = em.find(Team.class, team.getId());
            List<Member3> members = findTeam.getMembers();

            System.out.println(":::::::::::::::::::::::");
            for (Member3 member : members) {
                System.out.println("member : "+member.getUserName());
            }
            System.out.println(":::::::::::::::::::::::");

            et.commit();
        }

Transaction 이 DB 에 적용되기 이전에 Team에서 Member를 조회하는 경우 find 메소드로 조회가 되는 team 객체는 1차 cache에서 조회되는 객체로 member 가 할당되지 않은 상태이기 때문에
member 가 조회되지 않는다.

main : Member 와 Team 둘다 값을 할당하는 경우

try
        {
            Team team = new Team();
            team.setTeamName("A TEAM");
            em.persist(team);

            Member3 member3 = new Member3();
            member3.setUserName("member3");
            member3.setTeam(team);
            em.persist(member3);

            team.getMembers().add(member3);

            Team findTeam = em.find(Team.class, team.getId());
            List<Member3> members = findTeam.getMembers();

            System.out.println(":::::::::::::::::::::::");
            for (Member3 member : members) {
                System.out.println("member : "+member.getUserName());
            }
            System.out.println(":::::::::::::::::::::::");

            et.commit();
        }

이때 team.getMembers().add(member3);

다음 코드 처럼 선언하는 경우 코드가 복잡해지고 빼먹는 경우가 많기 때문에

@Entity
public class Member3
{
...
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }

다음과 같이 setter 에 한쪽에서 연관관계가 Mapping 될 때 동시에
진행될 수 있게 따로 정의한다.

양방향 Mapping 시 무한루프를 조심해라

toString 이나 ToString 을 정의해주는 Lombok, JSON 라이브러리 등을 사용할 때 양방향 관계의 Entity 를 사용하는 경우 무한루프가 발생하지 않도록 조심해야한다. ToString 으로 예를 들어보자

Team

@Override
    public String toString() {
        return "Team{" +
                "id=" + id +
                ", teamName='" + teamName + '\'' +
                ", members=" + members +
                '}';
    }

Member

@Override
    public String toString() {
        return "Member3{" +
                "id=" + id +
                ", userName='" + userName + '\'' +
                ", team=" + team +
                '}';
    }

main

        try
        {
            Team team = new Team();
            team.setTeamName("A TEAM");
            em.persist(team);

            Member3 member3 = new Member3();
            member3.setUserName("member3");
            member3.setTeam(team);
            em.persist(member3);

            team.getMembers().add(member3);

            Team findTeam = em.find(Team.class, team.getId());
            System.out.println(findTeam);


            et.commit();
        }

Team에서 toString 을 선언할때 member 안의 toString Method 가 선언이 되고 거기 안에서 다시 Team 필드에 대한 toString 메소드가 실행되는 무한루프가 발생할 수 있다 .

사실상 ..

양방향 Mapping 은 Table 구조에 영향을 주지 않기 때문에
단방향으로 객체 설계를 한다음 필요 시 양방향으로 변환하는 방법이 좋다고 한다.

0개의 댓글