[JPA 기초] 빠른 복습용 글(1)

식빵·2022년 9월 20일
0

JPA 이론 및 실습

목록 보기
15/17

회사에서 JPA 를 안 쓰다보니, 오랜만에 개인 프로젝트를 간단히 만드는데 조금 고생을 하고 있다.(현재진행형)

그래서 이번 기회에 JPA 를 아~주 빠르게 복습할 수 있는 글을 작성해보려 한다.
굉장히 많은 내용을 함축 표현함으로 나만 알아 볼지도 모르는 점 양해바란다.


1-1. 순수 Java 환경의 JPA 세팅법


✒ pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>me.dailycode</groupId>
    <artifactId>jpa-playground</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.6.10.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-hikaricp</artifactId>
            <version>5.6.10.Final</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.22.0</version>
        </dependency>
    </dependencies>
</project>



✒ persistence.xml

작성 경로: src/main/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="jpa_playground">
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
            <property name="javax.persistence.jdbc.user" value="jpa"/>
            <property name="javax.persistence.jdbc.password" value="jpa"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/jpa_playground"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL10Dialect"/>
            <property name="hibernate.physical_naming_strategy" value="org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy"/>

            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
          
          	<!-- 자동 DDL 기능 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>
  • 참고로 위에 딱히 커넥션 풀 내용은 없지만 pom.xml 에 hibernate-hikaricp 추가하면 자동으로 hikaricp 를 사용한다.



✒ logback.xml

작성 경로: src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(%-4relative) --- [ %thread{10} ]
                %cyan(%logger{20}) : %msg%n
            </pattern>
        </encoder>
    </appender>

    <logger name="org.hibernate.SQL" level="info"/>

    <root level="info">
        <appender-ref ref="CONSOLE"/> <!-- Console에 로그를 출력하고자 할 때 사용 -->
    </root>
</configuration>





1-2. JPA 테스트용 main 문 작성법

package me.dailycode.main;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaBasicMain {
	
	public static void main(String[] args) {
    
    	// persistence.xml ==> <persistence-unit name="jpa_playground">
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa_playground");
		EntityManager em = emf.createEntityManager();
        
        // JPA 에 의한 데이터 변경시 반드시 트랜잭션 내에서 행해야 한다.
		EntityTransaction tx = em.getTransaction();
		tx.begin();
        
		try {
			
			// JPA 코드 작성 지점
			
			tx.commit();
		} catch (Exception e) {
			e.printStackTrace();
			tx.rollback();
		} finally {
			em.close();
		}
		emf.close();
	}
}

이렇게만 하고 실행해도 아래처럼 뭔 정상 작동하는 것을 확인할 수 있다.





1-3. ENTITY 작성

https://www.baeldung.com/jpa-entities

Entity ? JPA에서 DB Table 저장되어 있는 데이터를 표현하는 클래스라고 생각하면 된다.

JPA 의 Entity 제약조건
1. 파라미터가 없는 public(또는 protected) 생성자가 필요
2. primary key 필드 위에 @Id 명시적 작성
3. final class ❌

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDate;

@Entity
@SequenceGenerator(
	schema = "public",	// DB 내의 스키마 이름
	sequenceName = "employee_seq",	// DB 내의 시퀀스 명
	name = "public.employee_seq"	// JPA 프레임워크가 사용할 시퀀스 명칭
)
@Getter
@NoArgsConstructor
public class Employee {
	
	@Id
	@GeneratedValue(strategy = SEQUENCE, generator = "public.employee_seq")
	private Long id;
	private String name;
	private LocalDate hireDate;
	
	public Employee(String name, LocalDate hireDate) {
		this.name = name;
		this.hireDate = hireDate;
	}
	
	public void changeEmpName(String name) {
		this.name = name;
	}	
}
/*
JpaBasicMain.main 문 돌리면 아래와 같이 DDL 이 생성 및 실행됨
Hibernate: create sequence public.employee_seq start 1 increment 50
Hibernate: 
    
    create table employee (
       id int8 not null,
        hire_date date,
        name varchar(255),
        primary key (id)
    )
*/

참고: @GeneratedValue(ID 생성전략) 은 이전에 작성한 JPA 이론 공부글에 잘 정리해놨다.



1-4. CRUD syntax

// JPA 코드 작성 지점
// 등록
Employee dailyCode = new Employee("dailyCode", LocalDate.now());
em.persist(dailyCode);

flushAndClear(em);

// 조회 [단건/다건]
Employee employee = em.find(Employee.class, dailyCode.getId());
List<Employee> employeeList = em
								.createQuery("select em from Employee em", Employee.class)
								.getResultList();

flushAndClear(em);

// 영속성 컨텍스트에 최초로 엔티티가 관리대상이 되면 SnapShot 이 찍히고,
// flush 시점에 해당 엔티티가 SnapShot 과 다르면 update 쿼리가 수행된다.
// 이런 걸 변경감지(Dirty Checking)라고 한다.
Employee findEmployee = em.find(Employee.class, dailyCode.getId());
findEmployee.changeEmpName("changedName");

flushAndClear(em);

Employee removeFind = em.find(Employee.class, dailyCode.getId());
em.remove(removeFind);

tx.commit();


/*
private static void flushAndClear(EntityManager em) {
	em.flush();
	em.clear();
	
	System.out.println("""
		\n!!!! flush has bean done !!!!
		""");
}
*/




1-5. Persistence Context? EntityManager?

https://www.baeldung.com/jpa-hibernate-persistence-context 참고

An EntityManager instance is associated with a persistence context. A persistence context is a set of entity instances in which for any persistent entity identity there is a unique entity instance. Within the persistence context, the entity instances and their lifecycle are managed. The EntityManager API is used to create and remove persistent entity instances, to find entities by their primary key, and to query over entities.

Persistence Context 는 DB 에서 조회하거나 변경(등록,수정,삭제)을 일으킬 엔티티 인스턴트를 캐싱하는 공간이며, 이 공간에 들어온 엔티티는 Persistence Context 에 의해서 생명주기가 관리된다. 이 추상적인 캐싱 공간은 애플리케이션과 DB 사이에 존재한다.

참고 : 이 공간에 놓이는 엔티티들은 모두 서로 고유하며, 그 유니크함의 기준은 Entity Class 의 @Id 에 의해서 결정된다.

EntityManager 는 이런 Persistence Context 에 접근 및 제어하기 위한 API 를 제공하는 클래스이다.




1-6. 엔티티 생명주기

Persistence Context 는 엔티티의 생명주기를 관리하게 된다.
그렇다면 생명 주기에는 어떤 것들이 있는가?

  • 비영속(new/transient) : Persistence Context 와 관련없는 상태
  • 영속(managed) : Persistence Context에 저장된 상태, em.persist 할 때 발생
  • 준영속(detached) :
    • Persistence Context 에 저장되었다가 분리된 상태
    • Persistence Context 상에서만 삭제된 것으로 이해하면 된다.
    • Persistence Context 에서 지워졌으므로, 어떠한 관리(등록/수정/삭제)도 발생 ❌
  • 삭제(removed)
    • Persistence Context 에서도 분리
    • 실제 DB 에서도 삭제가 예정된 상태
    • em.remove 할 때 발생

간단하게 아래처럼 쿼리를 코드를 돌려서 확인 가능

// JPA 코드 작성 지점
// 등록
Employee dailyCode = new Employee("dailyCode", LocalDate.now());
em.persist(dailyCode);
System.out.println(em.contains(dailyCode)); // true

flushAndClear(em);

Employee findEmployee = em.find(Employee.class, dailyCode.getId());
em.remove(findEmployee);

System.out.println(em.contains(findEmployee)); // false





1-7. ORM 매핑 애노테이션

가장 많이 보는 것만 작성합니다. 세세한건 필요할 때 찾아보기!


Entity Class 예시

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(schema = "public", name = "portal_user")
public class PortalUser {
	
	@Id
	private Long id;
	
	@Column(unique = true, nullable = false, length = 100) // length 는 문자열만 가능!
	private String nickName;
	
	@Column(name = "user_age", nullable = false)
	private Integer age;
	
	@Column(precision = 4, scale = 1) // BigDecimal, BigInteger 만 적용됨! 정밀한 계산에 사용
	private BigDecimal bigNum;
	
	@Column(name = "some_num", columnDefinition = "numeric(2,0) not null")
	private Integer num;
	
	@Enumerated(EnumType.STRING)
	private RoleType roleType;
	
	
	// 옛날 Date 타입에는 @Temporal 을 사용, 오늘날 LocalDateTime, LocalDate 에는 필요 X
	// @Temporal(TemporalType.TIMESTAMP)
	// private Date date;
	private LocalDate signupDate;
	
	@Lob // 문자열 타입이면 CLOB , 나머지는 BLOB. 참고로 postgresql 의 자동 ddl 은 oid 타입
	private String description;
	
	// TABLE 매핑을 원하지 않는 필드의 경우
	@Transient
	private int temp;
}

DDL 출력 내용

create table public.portal_user (
	id int8 not null,
	user_age int4 not null,
	big_num numeric(4, 1),
	description oid,
	nick_name varchar(100) not null,
	some_num numeric(2,0) not null,
	role_type varchar(255),
	signup_date date,
	primary key (id)
)
  • 참고: int 같은 기본 타입은 기본으로 not null 로 ddl 이 생성된다.
  • 참고: int 같은 기본 타입 필드 위에 @Column(nullable = true) 를 명시적으로 표기하면 실제로도 nullable 한 필드가 된다. 하지만 DB에서 읽어올 때 해당 컬럼이 Null 이면 PropertyAccessException 이 발생한다.

자동 생성되는 DDL 쿼리를 그대로 써도 될까?
아니다. 물론 많이 참고할 수는 있다. 하지만 primary key 의 constraint 이름을 주고 하는 등의 작업을 위해서 수동으로 SQL 을 사용해서 테이블을 만들고, 이후에 자동 스키마 생성 기능에서 <property name="hibernate.hbm2ddl.auto" value="validate" /> 로 바꾼 후, 서버를 기동해서 제대로 매핑됐는지 확인을 한다.




1-8. 매핑 연습

ERD 작성(연습용)

참고: https://app.diagrams.net/ 를 사용해서 ERD 만듦

  • Project_Team : (업무/사업) 프로젝트 팀
  • Programmer : 프로젝트에 참여한 프로그래머
  • Language : 프로그래밍 언어
  • Programmer_Language : 프로그래밍 언어와 프로그래머의 다대다 관계 링크 테이블
    • proficiency : "숙련도" 를 의미한다. 해당 언어를 얼마나 숙달되었는지를 알려주는 지표다. 이 컬럼의 값으로는 BRONZE, SILVER, GOLD 가 들어간다.

각 테이블의 ID 는 Sequence 를 따로 생성해서 적용한다.
시퀀스 명은 각 테이블 이름 + "_SEQ" 로 할 것이다.

스키마 따로 생성 및 재접속 시 search_path 자동 설정

psql -u postgres // 로그인!

postgres=# \c jpa_playground
jpa_playground=# create schema if not exists programmer authorization jpa;
jpa_playground=# ALTER ROLE jpa SET search_path = programmer,public;



Entity 작성

@Entity
@Table(schema = "programmer", name = "project_team")
@SequenceGenerator(
	schema = "programmer",
	sequenceName = "project_team_seq",
    name = "pgmr.project_team_seq"
)
@ToString(exclude = {"programmers"}) @Getter
@NoArgsConstructor 
public class ProjectTeam {
	
    public ProjectTeam(String teamName, LocalDate createDate) {
		this.teamName = teamName;
		this.createDate = createDate;
	}
    
	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "pgmr.project_team_seq")
	@Column(name = "project_team_id")
	private Long id;                // 아이디
	
	@Column(nullable = false)
	private String teamName;        // 프로젝트 팀 이름
	
	@Column(nullable = false)
	private LocalDate createDate;   // 프로젝트 팀 창단일
	
	@OneToMany(mappedBy = "projectTeam")
	private List<Programmer> programmers = new ArrayList<>();
    
    //"ArrayList로 초기화 해두는 것은 JPA 관례로써,
    // add할 때 NPE 발생을 막기 위해 사용한다"
    // - 김영한 선생님
}
@Entity
@Table(schema = "programmer", name = "programmer")
@SequenceGenerator(
	schema = "programmer",
    sequenceName = "programmer_seq",
    name = "pgmr.programmer_seq"
)
@ToString(exclude = {"projectTeam", "programmerLanguageList"}) @NoArgsConstructor @Getter
public class Programmer {
	
    public Programmer(String name) {
		this.name = name;
	}
    
	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "pgmr.programmer_seq")
	@Column(name = "programmer_id")
	private Long id;
	
	@ManyToOne(fetch = LAZY)
	@JoinColumn(name = "project_team_id")
	private ProjectTeam projectTeam;    // 소속 팀
	
	@Column(nullable = false)
	private String name;    // 프로그래머 이름
	
	@OneToMany(mappedBy = "projectTeam", cascade = CascadeType.ALL)
	private List<ProgrammerLanguage> programmerLanguageList = new ArrayList<>();
	
}
@Entity
@Table(schema = "programmer", name = "programmer_language")
@SequenceGenerator(
	schema = "programmer", 
    sequenceName = "programmer_language_seq", 
    name = "pgmr.prog_lang_seq"
)
@Getter @ToString(exclude = {"programmer"})
public class ProgrammerLanguage {
	
	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "pgmr.prog_lang_seq")
	@Column(name = "programmer_language_id")
	private Long id;
	
	@ManyToOne(fetch = LAZY, optional = false)
	@JoinColumn(name = "programmer_id")
	private Programmer programmer;
	
	@ManyToOne(fetch = LAZY, optional = false)
	@JoinColumn(name = "language_id")
	private Language language;
	
	@Enumerated(STRING)
	@Column(name = "proficiency", columnDefinition = "varchar(255) not null default 'BRONZE'")
	private LEVEL level = LEVEL.BRONZE;
	
}
@Entity
@Table(schema = "programmer", name = "language")
@SequenceGenerator(
	schema = "programmer",
    sequenceName = "language_seq",
    name = "pgmr.language_seq"
)
@Getter
@ToString
public class Language {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "pgmr.language_seq")
	@Column(name = "language_id")
	private Long id;
	
	@Column(nullable = false)
	private String languageName;    // 프로그래밍 언어 (영어)명
	
	@Column(nullable = false)
	private String languageNameKo;  // 프로그래밍 언어 한글명
}

참 작성할 게 많다 ^^;;


자동 스키마 생성으로 만들어진 테이블

intellij ultimate 의 diagram 보기 기능을 사용한 것이다.

  • 초록색 점 : NOT NULL
  • 노란 열쇠 : Primary key
  • 파란색 열쇠 : Foreign Key




1-9. 연관관계 매핑

  • 연관관계 매핑?
    • 연관관계 매핑은 객체의 참조와 테이블 외래키를 매핑하는 것
  • 객체와 테이블 연관관계의 차이
    • 객체는 reference(참조), 테이블은 외래키로 관계를 맺음
    • 이런 참조라는 주제와 관련한 패러다임 불일치를 해결하기 위한 것이 연관관계 매핑
  • 연관관계 용어
    • 방향(direction): 단방향, 양방향
    • 다중성(Multiplicity): 다대일(N:1), 일대일(1:1), 다대다(N:M)
    • 연관관계의 주인(Owner) 정의 및 필요성
      • 테이블은 외래키 1개로 연관관계 맺음
      • 2개의 Entity 클래스는 서로의 참조값을 가짐으로써 연관관계를 맺을 수 있음
      • 이때 둘 중 어떤 Entity 클래스의 필드가 DB 테이블의 외래키를 관리할지를 결정해야함
      • 최종적으로 관리를 담당하게 되는 필드가 바로 외래키의 주인이다.
      • 외래키 주인은 외래키 등록/수정 가능하고, 주인이 아닌쪽은 읽기만 가능하다.
      • 만약에 단방향 연관관계면 신경쓰지 않아도 된다. 양방향일 때만 신경쓰면 된다.

  • TIP : 연관관계의 주인을 지정할때는 외래키가 있는 쪽, 즉 N:1 에서 N쪽을 주인으로 하자.
  • 양방향 매핑 시에는 순수 객체 상태도 고려해서 항상 양쪽에 값을 설정해준다.
    • 연관관계 편의 메소드 생성, 될 수 있으면 연관관계 메소드 양쪽 중 하나에만 작성하자.
    • 양쪽 다 작성하면 머리만 아프다.

연관관계 편의 메소드라는 표현은 "김영한 개발자님"의 저서에 나온 것이다.
공식적인 표현이 아니며, 그냥 양쪽 객체의 참조 필드에 세팅을 해주는 메소드이다.


  • 연관관계 편의 메소드 EXAMPLE
@Entity
public class Programmer {

	// ... 일부 생략 ...
    
	@ManyToOne(fetch = LAZY)
	@JoinColumn(name = "project_team_id")
	private ProjectTeam projectTeam;    // 소속 팀
 
	@OneToMany(mappedBy = "programmer")
	private List<ProgrammerLanguage> programmerLanguageList = new ArrayList<>();
	
	// 새로운 팀에 배정됨
	public void assignedToNewTeam(ProjectTeam projectTeam) {
		this.projectTeam = projectTeam;
		if (projectTeam != null) {
			projectTeam.getProgrammers().add(this);
		}
	}
	
	// 팀에서 나감
	public void outFromTeam() {
		if (this.projectTeam != null) {
			List<Programmer> programmers = this.projectTeam.getProgrammers();
			programmers.remove(this);
			this.projectTeam = null;
		}
	}
}
  • 주의사항: 양방향 연관관계는 항상 무한 루프를 주의하자.
  • 연관관계 메소드가 아니더라도, toString, Json 라이브러리 등을 사용하면 항상 주의하자.

  • 연관관계 있을 때 em.persist 방식
Programmer dailyCode = new Programmer("dailyCode");
Programmer coolGuy = new Programmer("coolGuy");
Programmer doomGuy = new Programmer("doomGuy");
Programmer hikari = new Programmer("hikari");

ProjectTeam portalService = new ProjectTeam("portal-service", LocalDate.now());
ProjectTeam adminService = new ProjectTeam("admin-service", LocalDate.now());

dailyCode.assignedToNewTeam(portalService);
coolGuy.assignedToNewTeam(portalService);

doomGuy.assignedToNewTeam(portalService);
hikari.assignedToNewTeam(adminService);

// CASCADE ALL 덕분에 ProjectTeam 엔티티 내에 저장된 
// Programmer List 의 엔티티도 자동으로 em.persist 된다.
em.persist(portalService);
em.persist(adminService);

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

ProjectTeam projectTeam = em.find(ProjectTeam.class, portalService.getId());
System.out.println("find programmers!");
for (Programmer programmer : projectTeam.getProgrammers()) {
	System.out.println("programmer = " + programmer);
}




1-10. 다중성과 연관관계의 주인

1-9 에서는 N:1 에서 N 쪽에 연관관계의 주인을 줬다.

이번에는 조금 다른 다중성의 종류로 연관관계의 주인을 다뤄보자.

  • 1:N 연관관계
  • 1:1 연관관계
  • N:M 연관관계


✒ 1:N 연관관계

그냥 코드로 만 작성함. 보면 암.

예제 도메인(1)

// 1:N 중에서 1 에 해당
@Entity
@Table(schema = "public")
@Getter @Setter
@ToString(exclude = "empList")
public class Company {
	
	@Id @GeneratedValue
	private Long id;
	
	private String companyName;
	
    // 자기가 매핑한 테이블에 없는 외래키에 대한 관리가 가능하다.
    // 참고로 @OneToMany 에 @JoinColumn 을 작성 안하면 JoinTable 전략이 실행된다. 주의.
	@OneToMany
	@JoinColumn(name = "team_id")
	private List<CompanyEmp> empList = new ArrayList<>();
	
}

예제 도메인(2)

// 1:N 중에서 N 에 해당
@Entity
@Table(schema = "public")
@Getter @Setter @ToString
public class CompanyEmp {

	@Id @GeneratedValue
	private Long id;
	private String name;
}

JpaBasicMain 코드 작성 및 테스트

CompanyEmp companyEmp = new CompanyEmp();
companyEmp.setName("dailyCode");

em.persist(companyEmp);

Company company = new Company();
company.setCompanyName("Naver");

// 외래키의 주인에 값을 주입
company.getEmpList().add(companyEmp);

em.persist(company);
Hibernate: 
    /* insert me.dailycode.main.domain._03.CompanyEmp
        */ insert 
        into
            public.company_emp
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert me.dailycode.main.domain._03.Company
        */ insert 
        into
            public.company
            (company_name, id) 
        values
            (?, ?)
Hibernate: 
    /* create one-to-many row me.dailycode.main.domain._03.Company.empList */ update
        public.company_emp 
    set
        team_id=? 
    where
        id=?
  • 현재 관리하려는 컬럼이 company 자신의 테이블에는 없으니 어쩔수 없이
    update 를 하는 것이다!
  • 성능이슈도 있겠지만 일단 헷갈린다.
  • 실무에서는 테이블이 무지막지하게 많고, 연관관계도 많다.
  • 그런데 이런 방식을 사용하기 시작하면? 답이 없다.
  • 웬만해서는 항상 N 쪽에 연관관계의 주인으로 하자. 사실상 이게 정석이다..

양방향을 하려면...?

스펙상 1:N의 양방향 매핑은 존재하지는 않지만, 야매(?)로는 아래처럼 가능하다.

@Entity
@Table(schema = "public")
@Getter @Setter @ToString
public class CompanyEmp {

	@Id @GeneratedValue
	private Long id;
	
	private String name;
	
	@ManyToOne
	@JoinColumn(name = "team_id", insertable = false, updatable = false)
	private Company company;
}




✒ 1:1 연관관계

주 테이블에 외래키가 없는 경우로 보겠다.
참고로 주테이블이란 1:1 관계에서 더 자주 Access 하는 테이블을 의미한다.
ex) 사용자(주 테이블) - 회원등급(부 테이블)


예제 도메인(1): 주 테이블

@Entity
@Table(schema = "public", name = "member")
@Getter
@Setter
@ToString
public class Member {
	
	@Id @GeneratedValue
	private Long id;
	
	private String username;
	
	@OneToOne(mappedBy = "member")
	@ToString.Exclude
	private Locker locker;
}

예제 도메인(2): 부 테이블

@Entity
@Getter
@Setter
@ToString
public class Locker {
	
	@Id @GeneratedValue
	private Long id;
	
	private String name;

	@OneToOne
	@JoinColumn(name = "locker_id")
	@ToString.Exclude
	private Member member;
	
}




✒ N:M 연관관계

예제 도메인

@Entity
public class Product {
	
	@Id
	@GeneratedValue
	private Long id;
	
	private String name;
	
    /*
    // 상세하게 작성하면 아래처럼....
    // @JoinColumn.referencedColumnName 은 양쪽의 테이블 컬럼 명작성. 생략가능
    // @JoinColumn.name 은 연결 테이블에서 사용될 외래키 명이다.
    @JoinTable(
		name="product_orders",
		joinColumns=
		@JoinColumn(name="product_id", referencedColumnName="id"),
		inverseJoinColumns=
		@JoinColumn(name="order_id", referencedColumnName="id")
	)
    */
    
    // 간단하게 하면 아래처럼만 해도 됨.
	@ManyToMany
	@JoinTable(name="product_orders")
	private Set<Order> order = new HashSet<>();
}

예제 도메인(2)

@Entity
@Table(name = "orders")
public class Order {
	
	@Id @GeneratedValue
	private Long id;
	
	private String orderNum;
	
	@ManyToMany(mappedBy = "order")
	private Set<Product> products = new HashSet<>();
	
}

생성된 테이블 모양새




✒ TIP: 자기 자신의 PK 를 외래키로 갖는 경우

@Entity
public class Category {

	@Id @GeneratedValue
	@Column(name = "category_id")
	private Long id;
	
	private String categoryNm;
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "parent_id")
	private Category parent;
	
	@OneToMany(mappedBy = "parent")
	private List<Category> childList = new ArrayList<>();
}

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글