java 스프링부트 ( spring boot ) / JPA 연관관계

김동명·2022년 12월 7일
0

스프링부트

목록 보기
14/19
post-thumbnail

프로젝트 세팅

  • resources > META-INF 폴더 생성 > persistence.xml 생성
<?xml version="1.0" encoding="UTF-8"?>

<persistence version="2.2"
   xmlns="http://xmlns.jcp.org/xml/ns/persistence"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
   <persistence-unit name="hello">
      <properties>
         <!-- 필수 속성 -->
         <property name="javax.persistence.jdbc.driver" value="oracle.jdbc.driver.OracleDriver" />
         <property name="javax.persistence.jdbc.user" value="springjpa" />
         <property name="javax.persistence.jdbc.password" value="springjpa" />
         <property name="javax.persistence.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
         <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect" />
		
        <!-- 테이블 생성 옵션 ( 필요할 때만 사용 ) -->
		<property name="hibernate.hbm2ddl.auto" value="create"/>
		
         <!-- 옵션 -->
         <!-- 콘솔에 하이버네이트가 실행하는 SQL문 출력 -->
         <property name="hibernate.show_sql" value="true" />
         <!-- SQL 출력 시 보기 쉽게 정렬 -->
         <property name="hibernate.format_sql" value="true" />
         <!-- 쿼리 출력 시 주석(comments)도 함께 출력 -->
         <property name="hibernate.use_sql_comments" value="true" />
      </properties>
   </persistence-unit>
</persistence>



ㅇ 이론

- 연관관계 매핑

- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 연관관계 주인 ( Owner ) 
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다.



시작

1.연관관계 매핑 예제


  • 예제
    - 회원 - 팀
    - 회원은 하나의 팀에만 소속 될 수 있다.
    - 회원과 팀은 다대일 관계다

1-1. 기본


  • domain 패키지 파일 @Entity 주석

  • relation 패키지 > Member.java 생성
@Entity
@Getter @Setter
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	@Column(name = "USERNAME")
	private String name;
	@Column(name = "TEAM_ID")
	private Long teamid;
}

  • relation 패키지 생성 > Team.java 생성
@Entity
@Getter @Setter
public class Team {

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


  • JpaMain.java 수정
...
		try {
			
			Team team = new Team();
			team.setName("TeamA");
			// 영속상태가 되면, PK의 값이 세팅이 된 후 영속상태가 된다.
			em.persist(team);
			
			Member member = new Member();
			member.setName("member1");
			member.setTeamId(team.getId());
			em.persist(member);
			
			
			// select
			// 어느팀 소속인지 알고 싶을 때 JPA or DB에게 계속 물어봐야 한다.
			Member findMember = em.find(Member.class, member.getId());
			Long findTeamId	= findMember.getTeamId();
			Team findTeam = em.find(Team.class, findTeamId);
			System.out.println("findTeam : " + findTeam.getName());
			
			
			tx.commit();
         ...

  • 결과 ( JpaMain.java 실행 시 )
    • ( 콘솔창 )
    • ( DBeaver 에서 SELECT * FROM TEAM t; )
    • ( DBeaver 에서 SELECT * FROM "MEMBER" m; )
    • ( DBeaver 에서 SELECT * FROM TEAM t LEFT OUTER JOIN "MEMBER" m ON t.TEAM_ID = m.TEAM_ID; )



1-2. 연관관계 매핑 ( 1 : N ) 단방향 매핑

  • relation 패키지

  • Member.java 수정
@Entity
@Getter @Setter
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	@Column(name = "USERNAME")
	private String name;
//	@Column(name = "TEAM_ID")
//	private Long teamId;
	
	@ManyToOne  // Team 이 하나 
	@JoinColumn(name = "TEAM_ID") // 관계 컬럼을 적어준다. TEAM_ID와 조인해야 한다.
	private Team team;
}
  • JpaMain.java 수정
...
		try {
			
			Team team = new Team();
			team.setName("TeamA");
			// 영속상태가 되면, PK의 값이 세팅이 된 후 영속상태가 된다.
			em.persist(team);
			
			Member member = new Member();
			member.setName("member1");
//			member.setTeamId(team.getId());
			member.setTeam(team);
			em.persist(member);
			
			// 강제 db 쿼리를 보고 싶을때
			em.flush();
			em.clear();
			
			// select
			// find시에 1차캐시에서 가지고 와서 select 문이 없다.
			Member findMember = em.find(Member.class, member.getId());
			Team findTeam = findMember.getTeam();
			System.out.println("findTeam : " + findTeam.getName());
			
			
			tx.commit();
...   

  • 결과 ( JpaMain.java 실행 시 )
    • ( 콘솔창 println )
    • ( 콘솔창 sql문)



1-3. 연관관계 매핑 ( 1 : N ) 양방향 매핑

이론

  • 양방향 매핑
    - 현재 객체는 Member가 Team을 가졌으나, Team은 Member를 가지지 못한다.
    - 객체 참조와 외래키의 가장 큰 차이점
    - 테이블은 FK만 있으면 양쪽의 연관관계를 알 수 있다.
    - Member : Team = : n : 1 => @ManyToOne
    - Team : Member = 1 : n => @OneToMany

  • 테이블 연관관계
    - 관계 1개
    - Member 테이블 입장에서 Team 테이블 조인 가능
    - Team 테이블 입장에서 Member 테이블 조인 가능

  • 객체 연관관계
    - 관계 2개
    - Member 객체에서 Team 객체로 연관관계 1개 ( 단방향 )
    - Team 객체에서 Member 객체로 연관관계 1개 ( 단방향 )

  • 관리의 딜레마
    - 둘 중 하나로 외래키를 관리해야 한다.
    - Member에서 Team으로 가는 Team 참조 값과, Team에서 Member로 가는 member 참조 값이 있다.
    - Member에서 Team값이 수정 되었을 때 Member table의 TEAM_ID가 수정되야 하는지, Team에 있는 members를 수정했을 때 Member table의 TEAM_ID가 수정되야 하는지?
    -> DB 입장에서는 Member table에 있는 TEAM_ID만 update하면 된다 -> 룰 ( 주인 ) 이 생긴다.

  • 연관관계의 주인 ( Owner ) - 양방향 매핑 규칙
    - 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
    - 연관관계의 주인만이 외래 키를 관리 ( 등록, 수정)
    - 주인이 아닌쪽은 읽기만 가능
    - 주인은 mappedBy 속성 사용 x
    -> mappedBy : 내가 누군가에 의해서 mapping 되었다는 뜻
    - 주인이 아니면 mappedBy 속성으로 주인 지정

  • Team.java 수정
    - Member.java에 접근 할 수 있도록 참조
    • Team은 Member를 여러개 가지고 있어야 함
    • ArrayList로 정의 > 여러개의 Member에 접근할 수 있어야 하기 때문
    • @OneToMany(mappedBy = "team") > Member.java에 선언된 객체명 team에 매핑
@Entity
@Getter @Setter
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	private String name;
    
	@OneToMany(mappedBy = "team")
	private List<Member> member = new ArrayList<Member>();
	
}

  • persistence.xml
	     <property name="hibernate.hbm2ddl.auto" value="create"/>

  • JpaMain.java 수정
...
		try {
			
			Team team = new Team();
			team.setName("TeamA");
			// 영속상태가 되면, PK의 값이 세팅이 된 후 영속상태가 된다.
			em.persist(team);
			
			Member member = new Member();
			member.setName("member1");
//			member.setTeamId(team.getId());
			member.setTeam(team);
			em.persist(member);
			
            // 이때까진 Member 객체에만 team이 할당되어있고
            // Team 객체에는 Member가 할당되어있지않다. 따라서
			// 두 데이터를 DB에서 다시 받아오게 하기위함
			em.flush();
			em.clear();
			
			// select
			// find시에 1차캐시에서 가지고 와서 select 문이 없다.
			Member findMember = em.find(Member.class, member.getId());
			Team findTeam = findMember.getTeam();
			System.out.println("findTeam : " + findTeam.getName());
			
			// 양방향 매핑
			Member findSideMember = em.find(Member.class, member.getId());
			List<Member> members = findSideMember.getTeam().getMember();
			
			for( Member m : members) {
				System.out.println("result : " + m.getName());
			}
			
			tx.commit();
...




2. 연관관계 매핑 주의 할 점

  • 양방향 연관관계에서 주의 할 점
    - 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정해 주어야 한다.
    - 연관관계 편의 메서드를 생성해 주어야 한다.
    - 양방향 매핑시에 무한 루프를 조심해야 한다.
    -> toString(), lomlok lib를 조심해야 한다.

2-1. insert하는 순서 정하기

  • JpaMain2.java 생성
    • 양방향 매핑시 가장 많이 하는 실수
    • 오너에 먼저 insert 하기
public class JpaMain2 {

	public static void main(String[] args) {
		
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();
		tx.begin();
		
		try {
			
			/*
			 *  양방향 매핑시 가장 많이 하는 실수
			 */
			Member member = new Member();
			member.setName("member1");
			em.persist(member);
			
			Team team = new Team();
			team.setName("TeamA");
			team.getMember().add(member);
			em.persist(team);
			
			em.flush();
			em.clear();
			
			tx.commit();
		} catch (Exception e) {
			tx.rollback();
		} finally {
			em.close();
			emf.close();
		}
	}
}

2-2. DB를 통해 값 가져오기

  • JpaMain2.java 수정
    • team 객체에는 값이 들어가 있지 않은상태
    • DB를 통해서만 값을 가져 올 수 있다.
...
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team);
			
			
			Member member = new Member();
			member.setName("member1");
			member.setTeam(team);
			em.persist(member);
			

			em.flush();
			em.clear();
			
			System.out.println("========================================");
			
			Team findTeam = em.find(Team.class, team.getId());
			List<Member> members = findTeam.getMember();
			
			for( Member m : members ) {
				System.out.println("m : " + m.getName());
			}
			
			System.out.println("========================================");
			
...
  • 출력

2-3. DB를 거치지 않고 값 가져오기

  • JpaMain2.java
    • team 객체에는 값을 따로 넣어주기
    • flush(),clear() 주석
    • DB를 통하지 않고도 값을 가져 올 수 있다.
...
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team);
			
			Member member = new Member();
			member.setName("member1");
			member.setTeam(team);
			em.persist(member);

//			em.flush();
//			em.clear();
			
			// 객체 지향적인 입장에서 양쪽에 모두 값을 넣어주어야 한다.
			// 양방향 매핑시에는 양쪽에 값을 모두 입력해주어야 한다.
			// DB를 다시 다녀오지 않고 객체 상태로만 사용 할 수 있다.
			team.getMember().add(member);
			
			System.out.println("========================================");
			
			Team findTeam = em.find(Team.class, team.getId());
			List<Member> members = findTeam.getMember();
			
			for( Member m : members ) {
				System.out.println("m : " + m.getName());
			}
			
			System.out.println("========================================");
...
  • 출력

2-4. 위 과정들을 더 간단하게 변경하기 ( Member.java 수정 )

  • Member.java
  • @Setter(value = AccessLevel.NONE) : lombok에서 자동 setter 생성을 막아준다.
...
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	@Setter(value = AccessLevel.NONE) 
	private Team team;
    
    // 일반적으로 setter의 형태가 아니면 메서드 이름을 바꿔준다.
	// 추후 소스코드를 봤을 때 단순 setter의 작업이 아닌 중요한 작업을 진행하는지를 파악 할 수 있다.
	public void changeTeam(Team team) {
		this.team = team;
		team.getMember().add(this); // this : 나 자신의 인스턴스를 넣어준다.
	}
  • JpaMain2.java
...
			Member member = new Member();
			member.setName("member1");
//			member.setTeam(team);
			member.changeTeam(team);
			em.persist(member);
			

//			em.flush();
//			em.clear();

//			team.getMember().add(member);
			
			System.out.println("========================================");
...

2-5. 위 과정들을 더 간단하게 변경하기 ( Team.java 수정 )

  • team에서 하는 방법

  • Team.java 수정
...
	public void addMember (Member member ) {
		member.setTeam(this);
		this.member.add(member);
	}
	
...

  • JpaMain.java 수정
...
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team);
			
			
			Member member = new Member();
			member.setName("member1");
//			member.setTeam(team);
//			member.changeTeam(team);
			em.persist(member);
			
			team.addMember(member);


...

무한루프

-> Member.java , Team.java에 각각 toString 만들어주기
@ToString 어노테이션이나 @DATA 어노테이션을 이용하게되면 고정된 toString값이 나오므로 서로가 서로를 끊임없이 바라보는 무한루프 발생 가능 lomlok lib를 조심해야 한다.


  • Member.java 수정
...
	@Override
	public String toString() {
		return "Member [id=" + id + ", name=" + name + ", team=" + team + "]";
	}
...

  • Team.java 수정
...
	@Override
	public String toString() {
		return "Team [id=" + id + ", name=" + name + ", member=" + member + "]";
	}
...



3. 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관께 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회기능이 추가 된 것 뿐.
  • 양방향 사용 이유 : JPQL에서 역 방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘 하고 양방향은 필요 할 때 추가해도 됨 ( 테이블에 영향을 주지 않음 )

    - 결론: 객체 입장에서 양방향 매핑이 필수는 아님.

  • 연관관계의 주인을 정하는 기준
    - 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
    - 연관관계의 주인은 외래 키의 위치를 기준으로 정해야함

Member.java / Team 의 @Entity 주석처리

java 스프링부트 ( spring boot ) JPA 연관관계 ( 2 )

profile
코딩공부

0개의 댓글