JPA 기본 정리

1. JPA 소개

JPA (Java Persistence API)

  • 자바 진영의 ORM 기술 표준
  • ORM (Object-relational mapping, 객체 관계 매핑)
    • 객체는 객체대로 설계
    • 관계형 데이터 베이스는 관계형 데이터 베이스대로 설계
    • ORM 프레임 워크가 중간에서 매핑
    • 대중적인 언어에서는 대부분 지원함
  • JPA 동작
    • 객체의 엔티티에 대한 CURD 요청 → JPA 스스로 객체에 대한 CRUD 생성
    • 패러다임의 불일치 해결
  • EJB → Hibernate → JPA
  • JPA는 표준 명세
    • JPA는 인터페이스의 모음
    • JPA 2.1 표준 명세를 구현한 3가지 구현체
    • 하이버네이트, EclipseLink, DataNucleus

JPA를 왜 사용해야 하는가?

  • 객체 중심으로 개발
  • 생산성, 유지보수
  • 패러다임의 불일치
  • 성능, 표준
  • 생산성 - JPA 와 CRUD
    • 저장 : jpa.persist(member);
    • 조회 : jpa.find(memberId);
    • 수정 : member.setName(”이름”);
    • 삭제 : jpa.remove(member);
  • JPA와 패러다임의 불일치 해결
    • 상속
      • 알아서 조인 등 상속 관계 체크하여 가져옴
    • 연관 관계
    • 객체 그래프 탐색
      • 신뢰할 수 있는 엔티티, 계층
    • JPA와 비교하기
      • 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장한다.
  • JPA의 성능 최적화 기능
    • 1차 캐시와 동일성 보장
      • 같은 트랜잭션 안에서는 같은 엔티티를 반환
      • DB Isolation Level이 Read Commit 이어도, 애플리케이션에서 Repeatable Read 보장
    • 트랜잭션 지원하는 쓰기 지연
      • 트랜잭션을 커밋할 떄 까지 INSERT SQL 을 모음.
      • JDBC BATCH SQL 기능을 사용해 한번에 SQL 전송
    • 지연 로딩
      • UPDATE,DELETE로 인한 로우 락 최소화
      • 실행하고 바로 커밋함.
  • 지연로딩과 즉시로딩
    • 지연로딩 → 객체가 실제 사용될 때 로딩
    • 즉시로딩 → JOIN SQL로 한번에 연관된 객체까지 미리 조회

2. JPA 실습

데이터베이스 방언

  • JPA 는 특정 데이터베이스에 종속되지 않는다.
  • 각각의 데이터베이스가 제공하는 SQL 문법과 함수는 조금씩 다름
    • 가변 문자: MySQL은 VARCHAR, Oracle은 VARCHAR2
    • 문자열을 자르는 함수: SQL 표준은 SUBSTRING(), Oracle은 SUBSTR()
    • 페이징: MySQL은 LIMIT , Oracle은 ROWNUM
  • 방언: SQL 표준을 지키지 않는 특정 데이터베이스만의 고유한 기능
  • hibernate.dialect 속성에 지정
    • H2 : org.hibernate.dialect.H2Dialect
    • Oracle 10g : org.hibernate.dialect.Oracle10gDialect
    • MySQL : org.hibernate.dialect.MySQL5InnoDBDialect
    • 하이버네이트는 40가지 이상의 데이터베이스 방언 지원

        <?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="org.h2.Driver"/>
                    <property name="javax.persistence.jdbc.user" value="sa"/>
                    <property name="javax.persistence.jdbc.password" value=""/>
                    <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
                    <!-- DB 종류에 따라 변경할 수 있다. -->
                    <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
                    <!-- 옵션 -->
                    <property name="hibernate.show_sql" value="true"/><!-- sql 보여주는 옵션 -->
                    <property name="hibernate.format_sql" value="true"/><!-- sql 이쁘게 하는 옵션 -->
                    <property name="hibernate.use_sql_comments" value="true"/><!-- sql 주석으로 어떤 의미인지 나타내는 옵션 -->
                    <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
                </properties>
            </persistence-unit>
        </persistence>
      

JPA 구동 방식

Untitled

  • 설정 정보를 조회 후, 엔티티 매니저 팩토리가 엔티티 매니저를 찍어내고, 그것을 사용하여 작업한다.
  • 객체 생성 및 사용하기
    • @Entity : 엔티티 객체를 의미(JPA에서 사용하겠다)
    • @Id : 키로 사용할 값 지정

      package hellojpa;
          
      import javax.persistence.Entity;
      import javax.persistence.Id;
          
      @Entity 
      public class Member {
          
          @Id
          private long id;
          private String name;
          
          public long getId() {
              return id;
          }
          
          public void setId(long id) {
              this.id = id;
          }
          
          public String getName() {
              return name;
          }
          
          public void setName(String name) {
              this.name = name;
          }
      }
      
  • 엔티티 매니저 사용 시 주의점!
    • 엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에 서 공유
    • 엔티티 매니저는 쓰레드간에 공유X (사용하고 버려야 한다).
    • JPA의 모든 데이터 변경은 트랜잭션 안에서 실행

      public static void main(String[] args) {
      				// 팩토리를 선언하고 초기화 한다.
              EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
      				// 팩토리에서 사용할 엔티티 매니저를 생성한다.
              EntityManager em = emf.createEntityManager();
      				// 데이터 변경은 트랜잭션 안에서 이루어져야 하므로, 트랜잭션을 생성 후 시작한다.
              EntityTransaction tx = em.getTransaction();
              tx.begin();
          
      				// 작업 ...
              try {
                  List<Member> result = em.createQuery("select m from Member as m", Member.class)
                          .setFirstResult(1)
                          .setMaxResults(8)
                          .getResultList();
          
                  for(Member member : result) {
                      System.out.println("Member Name is " + member.getName());
                  }
      						// 트랜잭션 커밋
                  tx.commit();
              } catch (Exception e) {
      						// 에러 발생하면 트랜잭션 롤백
                  tx.rollback();
              } finally {
      						// 엔티티 매니저 사용 종료
                  em.close();
              }
      				// 팩토리 클래스 사용 종료
              emf.close();
          }
      

JPQL

  • SQL 대신 사용하는 JPA의 문법
    • JPA를 사용하면 엔티티 객체를 중심으로 개발, 문제는 검색 쿼리
    • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
    • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능 → 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
  • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리, SQL은 데이터베이스 테이블을 대상으로 쿼리
  • 객체를 대상으로 하는 객체 지향 쿼리, SQL을 추상화하여 특정 데이터베이스 SQL에 의존하지 않는다.

3. 영속성 관리 - 내부 동작 방식

  • 엔티티 매니저 팩토리와 엔티티 매니저

    Untitled

영속성 컨텍스트

Untitled

  • 엔티티를 영구 저장하는 환경
  • persist(entity); → 엔티티를 영속성화 한다.
  • 논리적인 개념이며, 눈에 보이지 않는다.
  • 매니저를 통해 영속성 컨텍스트에 접근한다.

엔티티의 생명주기

Untitled

  • 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속 (managed) : 영속성 컨텍스트에 관리되는 상태
  • 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 (removed) : 삭제된 상태

영속성 컨텍스트의 이점

  • 1차 캐시

    Untitled

    • 캐시에 저장되어 나중에 조회할 때, 실제로 날리지 않고 캐시에서 조회
    • 트랜잭션 단위로 사용하기 때문에 누가 요청하여 트랜잭션 생성하고 나중에 종료될 때 사라진다.
    • 성능 이점은 크게 없으나 한 트랙잭션이 크고 복잡한 경우에 이러한 점이 도움이 됨.
  • 영속 엔티티의 동일성 보장
    • 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
  • 트랙잭션을 지원하는 쓰기지연(버퍼같은 느낌)
    • 트랜잭션을 커밋해야만 영속성으로 저장된 객체의 쿼리를 DB에 전달한다.
    • 쓰기 지연 SQL 저장소에 쿼리를 저장해놓았다가 커밋이 발생하면 쿼리를 실행한다.
    • 버퍼링 기능 사용
      • 모았다가 한번에 DB에 넣을 수 있게 한다.
    • 변경 감지

      Untitled

      • 별도의 업데이트 쿼리 없이, 자바 객체의 변경점이 일어나면 알아서 업데이트 쿼리를 날린다.
      • 스냅샷과 엔티티를 비교하여 변경점이 있으면 업데이트 쿼리를 생성하여 쓰기 지연 저장소에 저장해놓았다가 쿼리를 날린다.

        // 영속성 객체 변경
        Member member = em.find(Member.class, 1L);
        member.setName("updated");
        // --> 이렇게 객체가 변경이 되면, 자동으로 업데이트문을 생성하여 가지고 있음.
              
        // 따라서, em.persist할 필요도 없음!
        // em.persist(member);
        
    • 지연 로딩
      • 데이터 객체를 가져올 때 바로 가져오지 않고 나중에 가져오는 것.

4. 플러시

  • 영속성 컨텍스트의 내용을 데이터베이스에 반영
  • 영속성 컨텍스트를 플러시 하는 방법
    • em.flush() - 직접 호출
    • 트랜잭션 커밋 - 자동 호출
    • JPQL 쿼리 실행 - 자동 호출
  • 플러시를 하게 되면 1차 캐시가 사라지나? - NO!

    → JUST 반영만!

  • 플러시 모드 옵션
    • em.setFlushMode(opt);
    • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때, 플러시 기본 값
    • FlushModeType.COMMIT : 커밋할 때만 플러시
  • 플러시 모드의 특징
    • 영속성 컨텍스트를 지우지 않음
    • 영속성 컨텍스트 변경 내용을 데이터베이스에 동기화
    • 트랜잭션이라는 작업 단위가 중요! → 커밋 직전에만 동기화 하면 됨.
    • 왠만하면 AUTO 모드로 쓰는게 낫다.

5. 준영속 상태

  • 준영속 상태란?
    • 영속 → 준영속
      • 1차 캐시에 없으면 1차 캐시에 올린다(준영속 상태)
    • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리됨.
    • 영속성 컨텍스트가 제공하는 기능을 사용하지 못함
  • 준영속 상태로 만드는 방법
    • em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
    • em.clear() : 영속성 컨텍스트를 완전히 초기화
    • em.close() : 영속성 컨텍스트를 종료

6. 엔티티 매핑

객체와 테이블 매핑

  • 객체와 테이블 매핑 : @Entity, @Table
  • 필드와 컬럼 매핑 : @Column
  • 기본 키 매핑 : @Id
  • 연관관계 매핑 : @ManyToOne, @JoinTable

@Entity

  • 이 어노테이션이 붙은 클래스를 JPA가 관리, 엔티티라고 함
  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
  • 주의
    • 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
    • final 클래스, enum, interface, inner 클래스 사용X
    • 저장할 필드에 final 사용 X
  • 속성: name

      @Entity(name = "Member")
    
    • JPA에서 사용할 엔티티 이름을 지정한다.
    • 기본값: 클래스 이름을 그대로 사용(예: Member)
    • 같은 클래스 이름이 없으면 가급적 기본값을 사용한다

@Table

  • 엔티티와 매핑할 테이블을 지정한다.
  • 속성
    • name : 매핑할 테이블 이름
    • catalog : 데이터베이스 catalog 매핑
    • schema : 데이터베이스 schema 매핑
    • uniqueConstraints : DDL 생성 시에 유니크 제약조건 생성

데이터베이스 스키마 자동 생성

  • DDL을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심 -> 객체 중심
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용
  • 생성된 DDL은 운영 서버에서는 사용하지 않거나, 적절히 다듬은 후 사용
  • 속성 - hibernate.hbm2ddl.auto

      <!--데이터베이스 스키마 자동 생성 - 속성-->
      <!--<property name="hibernate.hbm2ddl.auto" value="create" /> -->
      <!--<property name="hibernate.hbm2ddl.auto" value="create-drop" />-->
      <!--<property name="hibernate.hbm2ddl.auto" value="update" />-->
      <!--<property name="hibernate.hbm2ddl.auto" value="validate" />-->
      <!--<property name="hibernate.hbm2ddl.auto" value="none" />-->
      <property name="hibernate.hbm2ddl.auto" value="create" />
    
    • create : 기존테이블 삭제 후 다시 생성 (DROP + CREATE)
    • create-drop : create와 같으나 종료시점에 테이블 DROP
    • update : 변경분만 반영(운영DB에는 사용하면 안됨)
    • validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
    • none : 사용하지 않음
  • 주의할 점
    • 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
    • 개발 초기 단계는 create 또는 update
    • 테스트 서버는 update 또는 validate
    • 스테이징과 운영 서버는 validate 또는 none
  • DDL 생성 기능
    • 제약조건 추가: 회원 이름은 필수, 10자 초과X
      • @Column(nullable = false, length = 10)
    • 유니크 제약조건 추가
      • @Table(uniqueConstraints = {@UniqueConstraint( name = “NAME_AGE_UNIQUE”, columnNames = {“NAME”, “AGE”} )})
    • DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다

필드와 컬럼 매핑

public class Member {
    @Id
    private Long id;
    @Column(name = "name")
    private String username;
    private Integer age;
    @Enumerated(EnumType.STRING)
    private RoleType roleType;
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    @Lob
    private String description;
}
  • @Column : 컬럼 매핑
  • @Temporal : 날짜 타입 매핑
  • @Enumerated : enum 타입 매핑
  • @Lob : BLOB, CLOB 매핑
  • @Transient : 특정 필드를 컬럼에 매핑하지 않음(매핑 무시)

@Column

  • name : 필드와 매핑할 테이블의 컬럼 이름
    • 기본 값 : 객체의 필드 이름
  • insertable, updatable : 등록, 변경 가능 여부
    • 기본 값 : TRUE
  • nullable(DDL) : null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다.
  • unique(DDL) : @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다.
  • columnDefinition (DDL) : 데이터베이스 컬럼 정보를 직접 줄 수 있다. ex) varchar(100) default ‘EMPTY’
    • 기본 값 : 필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입 지정
  • length(DDL) 문자 길이 제약조건, String 타입에만 사용한다.
    • 기본 값 : 255
  • precision, scale(DDL) : BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다).
    • precision은 소수점을 포함한 전체 자 릿수를, scale은 소수의 자릿수다.
    • 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다.
    • 기본 값 : precision=19, scale=2

@Enumerated

  • 자바 enum 타입을 매핑할 때 사용
  • 주의! ORDINAL 사용X
  • value
    • EnumType.ORDINAL: enum 순서를 데이터베이스에 저장
    • EnumType.STRING : enum 이름을 데이터베이스에 저장
    • 기본 값 : EnumType.ORDINAL

@Temporal

  • 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
  • 참고: LocalDate, LocalDateTime 타입을 사용하는 객체에서는 어노테이션을 안써도 인식하기 때문에 생략 가능(최신 하이버네이트 지원)
  • value
    • TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑 (예: 2013–10–11)
    • TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑 (예: 11:11:11)
    • TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스t imestamp 타입과 매핑(예: 2013–10–11 11:11:11)
  • @Lob
    • 데이터베이스 BLOB, CLOB 타입과 매핑
    • @Lob에는 지정할 수 있는 속성이 없다.
    • 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
      • CLOB: String, char[], java.sql.CLOB
      • BLOB: byte[], java.sql. BLOB

@Transient

  • 필드 매핑X
  • 데이터베이스에 저장X, 조회X
  • 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용

      @Transient
      private Integer temp;
    

기본 키 매핑

  • @id
    • 직접 할당
  • @GeneratedValue → @GeneratedValue(strategy = GenerationType.*AUTO)*
    • 자동 생성
    • IDENTITY: 데이터베이스에 위임, MYSQL
      • 기본 키 생성을 데이터베이스에 위임
      • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용 (예: MySQL의 AUTO_ INCREMENT)
      • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
      • AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행
      • IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행 하고 DB에서 식별자를 조회
      • 객체 타입은 int 보단 Integer 나 Long 추천, 자릿수 문제 등 때문에.
      • 단점
        • DB에 값이 들어가봐야 알 수 있다.
        • 아이덴티티 전략에서는 그래서, 영속성 관리할 때, persist() 만 해도 insert가 실행된다.
        • 그래서 모아서 insert 하는게 불가능하다.
    • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
      • @SequenceGenerator 필요, 주의: allocationSize 기본값 = 50

          | 속성 | 설명 | 기본값 |
          | --- | --- | --- |
          | name | 식별자 생성기 이름 | 필수 |
          | sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
          | initialValue | DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 1 시작하는 수를 지정한다. | 1 |
          | allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값 을 반드시 1로 설정해야 한다 | 50 |
          | catalog, schema | 데이터베이스 catalog, schema 이름 |  |   - 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)   - 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
                
          ```java
          @Entity@SequenceGenerator(
          			name = “MEMBER_SEQ_GENERATOR",
          			sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
          			initialValue = 1, allocationSize = 1)
          public class Member {
          	@Id@GeneratedValue(strategy = GenerationType.SEQUENCE,
          			generator = "MEMBER_SEQ_GENERATOR")
          	private Long id;
          }
          ```
        
        • 시퀀스 전략의 경우, 시퀀스가 만들어 진 후에 initialValue 과 allocationSize 에 의해서 초기값과 증가값이 결정되는데 처음 insert 할 때는 이 값을 모르기 때문에 DB에서 얻어와야 한다.
        • 그래서 영속성 컨텍스트를 persist() 하기 전에 시퀀스 값을 가져와서 얻은 후에 insert 한다.
        • allocationSize 기본값이 50인 이유?
          • 초기값 1, 증가 값 1 하는 경우에는 persist() 할 때마다 다음 시퀀스 값을 DB에서 가져와야 하는데
          • 초기값 1, 증가 값 50인 경우에는 증가값이 50인데 초기값이 1이니, 50씩 늘어나야 되는데 이상하다 판단하여 한번 더 seq의 next 값을 가져온다. 그렇게 되면 메모리에는 초기값 1, 다음값 51을 가지게 되는데, 실제 DB 시퀀스에서 증가 값을 1로 세팅하면 1~51 사이의 값을 다시 가져올 필요가 없기 때문에 메모리에서 1 다음인 2를 가져와 persist() 할 때 사용된다.
          • 따라서, 이러한 과정을 통해 성능 최적화를 할 수 있다.
      • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
        • @TableGenerator 필요
        • TABLE 전략
          • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉 내내는 전략
          • 장점: 모든 데이터베이스에 적용 가능
          • 단점: 성능
        • @TableGenerator - 속성

          속성설명기본값
          name식별자 생성기 이름필수
          table키생성 테이블명hibernate_sequences
          pkColumnName시퀀스 컬럼명sequence_name
          valueColumnNa시퀀스 값 컬럼명next_val
          me pkColumnValue키로 사용할 값 이름엔티티 이름
          initialValue초기 값, 마지막으로 생성된 값이 기준이다.0
          allocationSize시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)50
          catalog, schema데이터베이스 catalog, schema 이름 
          uniqueConstraint s(DDL)유니크 제약 조건을 지정할 수 있다. 
      • AUTO: 방언에 따라 자동 지정, 기본값
  • 권장하는 식별자 전략
    • 기본 키 제약 조건: null 아님, 유일, 변하면 안된다.
    • 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자. 예를 들어 주민등록번호도 기본 키로 적절하기 않다.
      • 권장: Long형 + 대체키 + 키 생성전략 사용

실전예제1 - 요구사항 분석과 기본 매핑 수행

  • 실전예제1 - 요구사항 분석과 기본 매핑
    • 왠만하면 객체에 길이나 인덱스, 유니크 키 등의 제약조건, 기반 조건들을 전부 매핑하는게 낫다.
    • 나중에 데이터베이스 열어보지 않고 객체만 보고 작업할 수 있도록 하는게 좋다.
    • 도메인 모델 분석
      • 회원과 주문의 관계: 회원은 여러 번 주문할 수 있다. (일대다)
      • 주문과 상품의 관계: 주문할 때 여러 상품을 선택할 수 있다. 반대로 같은 상품도 여러 번 주문될 수 있다. 주문상품 이라는 모델을 만들어서 다대다 관계를 일다대, 다대일 관계로 풀어냄.

        Untitled

    • 데이터 중심 설계의 문제점
      • 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
      • 테이블의 외래키를 객체에 그대로 가져옴
      • 객체 그래프 탐색이 불가능, 참조가 없으므로 UML도 잘못됨

7. 연관관계 매핑 기초

단방향 연관관계

  • 객체를 테이블에 맞춰서 모델링

    Untitled

    • 외래키인 TeamId를 그대로 객체에 적용하여 세팅함.
    • 이렇게 되면, 멤버와 연관이 있는 Team을 조회하려면 무조건 teamId를 알아야 함.
  • 객체 지향 모델링

    Untitled

    • 팀 자체를 멤버 객체에 적용함.
    • 객체 지향에서는 참조를 통한 관계가 이루어지기 때문.
    • 아래와 같은 설정을 통해, TEAM_ID 라는 매핑 컬럼을 주고 관계를 설정할 수 있음.

      @ManyToOne
      @JoinColumn(name = "TEAM_ID")
      private Team team;
      
    • 아래와 같이 관계가 이루어지게 됨. (ORM매핑)

    Untitled

    • 연관관계가 적용된 상태에서의 저장 및 수정

      //팀 저장
      Team team = new Team();
      team.setName("TeamA");
      em.persist(team);
          
      //회원 저장
      MemberOfTeam memberOfTeam = new MemberOfTeam();
                  memberOfTeam.setName("member1");
                  // memberOfTeam.setTeamId(team.getId());MemberOfTeam memberOfTeam = new MemberOfTeam();
                  memberOfTeam.setName("member1");
                  // memberOfTeam.setTeamId(team.getId());er = new Member();
      member.setName("member1");
      member.setTeam(team); //단방향 연관관계 설정, 참조 저장
      em.persist(member);
          
      //조회
      Member findMember = em.find(Member.class, member.getId());
      //참조를 사용해서 연관관계 조회
      Team findTeam = findMember.getTeam();
      

양방향 연관관계

  • 양방향 연관관계를 가지는 객체 타입

    Untitled

  • 객체와 테이블이 연관관계를 맺는 차이

    Untitled

    • 객체 연관관계 = 2개회원 -> 팀 연관관계 1개(단방향)
      • 팀 -> 회원 연관관계 1개(단방향)
    • 테이블 연관관계 = 1개
      • 회원 <-> 팀의 연관관계 1개(양방향)
  • 객체의 양방향 관계
    • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개다.
    • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

      // A -> B (a.getB())
      // B -> A (b.getA())
          
      // 위 처럼 코드가 되려면? 양 쪽에 선언해주어야 함.
      class A {
      	B b;
      }
          
      class B {
      	A a;
      }
      
  • 테이블의 양방향 관계
    • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
    • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐(양쪽으로 조인할 수 있다.)

      SELECT * FROM 
      	MEMBER M
      JOIN 
      	TEAM T 
      ON 
      	M.TEAM_ID = T.TEAM_ID
          
      SELECT * 
      FROM 
      	TEAM T
      JOIN 
      	MEMBER M 
      ON T.TEAM_ID = M.TEAM_ID
      
  • 둘 중 하나로 외래키 관리를 해야 한다.

    Untitled

연관관계의 주인(Owner)

  • 양방향 매핑 규칙
    • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
    • 주인은 mappedBy 속성 사용X
    • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
    • 주인이 아닌쪽은 읽기만 가능
    • 주인이 아니면 mappedBy 속성으로 주인 지정
  • 누구를 주인으로 잡아야 하는가?
    • 외래키가 있는 곳을 주인으로 정한다.

      Untitled

    • 잘못 설정하면 Team 에다가 작업을 했는데, Member 테이블의 업데이트 쿼리가 생성되는 경우가 발생.
    • 즉, 관계 상에서 동일한 외래키에 작업이 일어나는 것을 방지하기 위해 한 쪽에서 관리하는 것으로 정하는 것이다.
    • 1:N 관계에서 N 쪽이 주인이 된다.
    • 비즈니스적으로 중요한게 아니다, JPA를 사용함에 있어 객체와 테이블의 매핑 관계가 다르기 때문에 DB의 키 관리를 위한 작업임.
  • 양방향 매핑시 가장 많이 하는 실수
    • 연관관계의 주인에 값을 입력하지 않음

      // TODO: 양방향 관계 설정 시 가장 많이 하는 실수
      Team team2 = new Team();
      team2.setName("TeamA");
      em.persist(team2);
          
      MemberOfTeam member = new MemberOfTeam();
      member.setName("member1");
      member.setTeam(team2);
          
      team.getMembers().add(member);
                      
      em.persist(member);
      
    • 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.

  • 양방향 연관관계 주의 - 실습
    • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
    • 연관관계 편의 메소드를 생성하자
      • 일일히 코드 레벨에서 하기 귀찮기 때문에 getter & setter 같은 기본 값 세팅 레벨에서 코드를 추가해 놓으면 편한다.
      • setter 같은 네이밍은 이미 존재하는 것이기 때문에 change 같이 네이밍 변경해서 사용한다.
      • 각자 정하기 나름, 비즈니스로직은 매번 다르고 복잡하기 때문에 …
    • 양방향 매핑시에 무한 루프를 조심하자
      • 예: toString(), lombok, JSON 생성 라이브러리
      • 양 쪽으로 계속 무한으로 호출 가능성이 있는 자동 생성 메소드들을 조심하자.
  • 양방향 매핑 정리
    • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
      • 설계 단계에서 단방향 매핑으로 설계를 완료해야 한다.
    • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐.
    • JPQL에서 역방향으로 탐색할 일이 많음.
    • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨(테이블에 영향을 주지 않음)
  • 연관관계의 주인을 정하는 기준
    • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
    • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야함
      • 외래 키가 있는 테이블(객체)가 주인이다!

8. 다양한 연관관계 매핑

다대일(ManyToOne)

Untitled

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
    • 주 테이블에 외래 키
    • 대상 테이블에 외래 키
  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

다대일 단방향

Untitled

  • 가장 많이 사용하는 연관관계
  • 다대일의 반대는 일대다

다대일 양방향

Untitled

  • 반대쪽에 추가한다고 해서, 테이블에 영향 주지 않음.
  • 외래 키가 있는 쪽이 연관관계의 주인
  • 양쪽을 서로 참조하도록 개발

일대다(OneToMany)

  • 1 쪽에서 외래키를 관리하겠다.

일대다 단방향

Untitled

  • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음(설계상)
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
    • 실행하면, 관리 주체에서 반대 테이블의 FK를 어떻게 하지 못하기 때문에 관리 테이블 생성 후, 업데이트 문이 발생함.
  • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
  • 운영할 때 힘듦. 알지 못하는 업데이트 문이 발생 → 파악 못하면 힘들다.

일대다 단방향 매핑의 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
  • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

일대다 양방향

Untitled

  • 이런 매핑은 공식적으로 존재X
  • @JoinColumn(insertable=false, updatable=false)
    • 저 옵션을 걸지 않으면, 매핑이 2개가 되면서 어디를 먼저 해야 하는지 순서가 꼬이기때문에 false 처리 함.
  • 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
  • 다대일 양방향을 사용하자

일대일(OneToOne)

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
    • 주 테이블에 외래키
    • 대상 테이블에 외래키
  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

일대일 : 주 테이블에 외래키 단방향

Untitled

  • 다대일(@ManyToOne) 단방향 매핑과 유사

일대일 : 주 테이블에 외래키 양방향

Untitled

  • 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy 적용

일대일 : 대상 테이블에 외래 키 단방향

Untitled

  • 단방향 관계는 JPA 지원X
  • 양방향 관계는 지원

일대일 : 대상 테이블에 외래 키 양방향

Untitled

  • 사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같음

일대일 정리

  • 주 테이블에 외래키
    • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
    • 객체지향 개발자 선호
    • JPA 매핑 편리
    • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
    • 단점: 값이 없으면 외래 키에 null 허용
  • 대상 테이블에 외래키
    • 대상 테이블에 외래 키가 존재
    • 전통적인 데이터베이스 개발자 선호
    • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
    • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명)

다대다(ManyToMany)

Untitled

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
  • **객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능**

    Untitled

  • @ManyToMany 사용
  • @JoinTable로 연결 테이블 지정
  • 다대다 매핑: 단방향, 양방향 가능

다대다 매핑의 한계

Untitled

  • 편리해 보이지만 실무에서 사용X
  • 연결 테이블이 단순히 연결만 하고 끝나지 않음
  • 주문시간, 수량 같은 데이터가 들어올 수 있음

다대다 한계 극복

Untitled

  • 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
  • @ManyToMany -> @OneToMany, @ManyToOne
  • 중간 테이블에 @GeneratedValue 를 통해 PK ID 생성해 놓는게 더 편하다…

실전예제3 - 다양한 연관관계 매핑

  • 배송, 카테고리 추가

    스크린샷 2023-08-11 오전 12.22.09.png

    • 주문과 배송은 1:1(@OneToOne)
    • 상품과 카테고리는 N:M(@ManyToMany)
  • ERD

    Untitled

  • 엔티티 상세

    Untitled

N:M 관계는 1:N, N:1로

  • 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1
  • 실전에서는 중간 테이블이 단순하지 않다.
  • @ManyToMany는 제약: 필드 추가X, 엔티티 테이블 불일치
  • 실전에서는 @ManyToMany 사용X

@JoinColumn

  • 외래 키를 매핑할 때 사용

| 속성 | 설명 | 기본값 | | — | — | — | | name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테 이블의 기본 키 컬럼명 | | referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 | | foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | | | unique nullable insertable updatable columnDefinition table | @Column의 속성과 같다. | |

@ManyToOne - 주요 속성

  • 다대일 관계 매핑

| 속성 | 설명 | 기본값 | | — | — | — | | optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | TRUE | | fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER

  • @OneToMany=FetchType.LAZY | | cascade | 영속성 전이 기능을 사용한다. | | | targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거 의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타 입 정보를 알 수 있다. | |

@OneToMany - 주요 속성

  • 일대다 관계 매핑

| 속성 | 설명 | 기본값 | | — | — | — | | mappedBy | 연관관계의 주인 필드를 선택한다. | | | fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER

  • @OneToMany=FetchType.LAZY | | cascade | 영속성 전이 기능을 사용한다. | | | targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거 의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타 입 정보를 알 수 있다. | |

9. 고급 매핑

상속 관계 매핑

조인 전략

@Inheritance(strategy=InheritanceType.JOINED)

Untitled

  • 장점
    • 테이블 정규화
    • 외래 키 참조 무결성 제약조건 활용 가능
  • 단점
    • 조회 시 조인을 많이 사용, 성능 저하
    • 조회 쿼리가 복잡함
    • 데이터 저장 시, INSERT SQL 2번 호출

단일 테이블 전략

@Inheritance(strategy=InheritanceType.SINGLE_TABLE)

Untitled

  • 장점
    • 조인이 필요 없으므로, 일반적으로 조회 성능이 빠름
    • 조회 쿼리가 단순함
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 NULL 허용으로 해주어야 함.
    • 단일 테이블에 모든 것을 저장하므로, 테이블이 커질 수 있다.
    • 상황에 따라 성능이 오히려 저하될 수 있다.

구현 테이블 클래스 전략

@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) - 이 방식은 추천하지 않음.

Untitled

  • 장점
    • 서브 타입을 명확히 구분해서 처리할 때, 효과적
    • not null 제약 조건 사용 가능
  • 단점
    • 여러 자식들을 함께 조회할 떄 성능이 느리다. (union SQL 사용)
    • 자식 테이블을 통합해 쿼리하기 힘들다.

@MappedSuperClass

Untitled

공통 매핑 정보가 필요할 때 사용한다. (등록자,등록일,수정자,수정일 … 등등)

  • 특징
    • 상속관계 매핑X, 엔티티X, 테이블과 매핑X
      • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
      • 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지 정한 클래스만 상속 가능
    • 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
      • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통 으로 적용하는 정보를 모을 때 사용
    • 조회, 검색 불가(em.find(BaseEntity) 불가)
    • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장

10. 프록시와 연관관계

프록시 기초

  • em.find() vs em.getReference()
  • em.getReference() : 데이터베이스 실제 조회를 미루는 가짜 엔티티 객체를 조회.

프록시 특징

  • 실제 엔티티 클래스를 상속받아 만들어짐. 실제 모양은 똑같다.
  • 프록시 객체는 실제 객체의 참조를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
  • 프록시 객체는 처음 사용할 때, 한 번만 초기화 한다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님,초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능.
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생. (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

프록시 객체의 초기화

  • Member member = em.getReference(Member.class, “id1”); member.getName();
  • 프록시 객체의 메소드 호출로 초기화 시킨다.

Untitled

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인. PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인. entity.getClass().getName() 출력 → (..javasist.. or HibernateProxy…)
  • 프록시 강제 초기화 org.hibernate.Hibernate.initialize(entity); 참고: JPA 표준은 강제 초기화 없음. 강제 호출: member.getName() 와 같이 메소드 호출로 초기화 해줄 수 있지만 위 메소드를 이용함.

11. 즉시로딩과 지연로딩

지연로딩

  • 바로 조회 하지 않고, 실제로 사용할 때 조회함.
  • @ManyToOne(fetch = FetchType.*LAZY*) 와 같이, 관계에서 어노테이션 내 옵션 형태로 사용.

Untitled

즉시로딩

  • 즉시 연관된 테이블을 바로 조회함.
  • @ManyToOne(fetch = FetchType.*EAGER*) 와 같이 사용.

Untitled

주의할 점

  • 가급적 지연로딩만 사용한다.
  • 즉시 로딩을 적용하면 예상치 못한 쿼리가 나간다.
  • 즉시 로딩은 JPQL 에서 N + 1 문제를 일으킨다.
    • 즉시 로딩이 설정된 경우, 한 테이블 조회 시 쿼리 1개 나간 것 처럼보이나 나중에 사용할 때 갑자기 쿼리가 막 나감.
    • 조인 걸린 경우, 원 테이블에서 N개 조회하면 N개 만큼 쿼리가 더 나감.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 > LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!(뒤에서 설명)

12. 영속성 전이 : CASCADE

CASCADE

  • 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 같이 영속 상태로 만든다.
  • 부모 ↔ 자식 관계에서 많이 사용한다.
  • @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST) 와 같이 사용한다.

Untitled

영속성 전이 주의할 점.

  • 연관관계 매핑과는 아무 관련이 없다.
  • 편의성을 제공하는 기능이다.

CASCADE 종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH

고아객체

  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST, **orphanRemoval = true**)
  • 자식 엔티티를 제거
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);

주의할 점

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함! 특정 엔티티가 개인 소유할 때 사용
    • @OneToOne, @OneToMany만 가능
  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.

영속성 전이 + 고아객체, 생명주기

  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
...
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

// ALL 이라서, 부모만 저장해도 위에 자식들도 같이 저장됨.
em.persist(parent);

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

Parent findParent = em.find(Parent.class, parent.getId());
// 부모를 삭제한다 -> 자식도 같이 삭제 된다.
em.remove(findParent);
...
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음.
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용.

13. 값 타입

데이터 타입의 분류

  • 엔티티 타입
    • @Entity로 정의하는 객체.
    • 데이터가 변해도 식별자로 지속 추적이 가능.
    • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능.
  • 값 타입
    • int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경 시, 추적 불가.
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체

값 타입의 분류

  • 기본값 타입
    • 자바 기본 타입(int, double)
    • 래퍼 클래스(Integer, Long)
    • String
  • 임베디드 타입(embedded type, 복합 값 타입)
  • 컬렉션 값 타입(collection value type)

14. 임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입(embedded type)이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입

임베디드 타입이란?

Untitled

위 그림과 같이 묶어낼 수 있는 타입을 의미한다.

임베디드 타입 사용법

  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • 기본 생성자 필수

임베디드 타입의 장점

  • 재사용
  • 높은 응집도
  • Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함

임베디드 타입과 테이블 매핑

  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게(find-graind) 매핑이 가능하다.
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수 보다 클래스의 수가 더 많음.

임베디드 타입과 연관관계

Untitled

내부에 다른 엔티티 타입이나 값 타입을 가질 수 있다.

@AttributeOverride: 속성 재정의


  • 한 엔티티에서 같은 값 타입을 사용하면?
  • 컬럼 명이 중복됨
  • @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의

임베디드 타입과 NULL

  • 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null

15. 값 타입과 불변 객체

값 타입 공유 참조의 위험성

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
  • 부작용(side effect) 발생

값 타입 복사

  • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험
  • 대신 값(인스턴스)를 복사해서 사용

객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
    • 기본 타입(primitive type)

      int a = 10;
      int b=a;//기본 타입은 값을 복사
      b = 4;
      
    • 객체 타입

      Address a = new Address(Old);
      Address b = a; //객체 타입은 참조를 전달
      b. setCity(New)
      
  • 객체의 공유 참조는 피할 수 없다.

불변 객체

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
  • 값 타입은 불변 객체(immutable object)로 설계해야함
  • 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
  • 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체

16. 값 타입의 비교

  • 값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야함

      int a = 10;
      int b = 10;
        
      Address a = new Address(서울시);
      Address b = new Address(서울시);
        
      원래는 다르지만, 같다고 봐야 .
    

값 타입의 비교

  • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
  • 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드사용)

17. 값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함

값 타입 컬렉션 사용

  • 값 타입 저장 예제
  • 값 타입 조회 예제
    • 값 타입 컬렉션도 지연 로딩 전략 사용
  • 값 타입 수정 예제
  • 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

값 타입 사용 제약 사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: **null 입력X, 중복 저장X**

값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
  • EX) AddressEntity

18. 객체지향 쿼리 언어 - 기본

기본 소개

JPQL

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
    • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
    • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
    • JPQL은 엔티티 객체를 대상으로 쿼리
    • SQL은 데이터베이스 테이블을 대상으로 쿼리
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
String jpql = "select m From Member m where m.name like ‘%hello%'"; // 멤버 객체 자체를 가리키는 엔티티(m)를 가져오도록 쿼리.
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

쿼리가 단순 String 이기 때문에 동적 쿼리를 만들기가 힘들다.

Criteria - 동적 쿼리를 해결하기 위해 사용

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할JPA 공식 기능
  • 단점: 너무 복잡하고 실용성이 없다.
    • Criteria 대신에 QueryDSL 사용 권장
    • 유지보수가 힘듦. 알아보기도 힘들어…
//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Member> query = cb.createQuery(Member.class);
//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
//쿼리 생성 CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), “kim”)); List<Member> resultList = em.createQuery(cq).getResultList();

Query DSL

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
    • JPQL 빌더 역할
    • 컴파일 시점에 문법 오류를 찾을 수 있음
  • 동적쿼리 작성 편리함
  • 단순하고 쉬움
    • JPQL만 알면 잘 활용할 수 있어, JPQL 먼저 하고 하면 좋음
    • 공식 도큐먼트 설명이 매우 잘 되어있음 (http://querydsl.com/)
  • 실무 사용 권장
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em); QMember m = QMember.member;
List<Member> list =
    query.selectFrom(m)
         .where(m.age.gt(18))
         .orderBy(m.name.desc())
         .fetch();

Native SQL

  • JPA가 제공하는 SQL을 직접 사용하는 기능
  • JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
// Native SQL
String sql ="SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

DBC 직접 사용, SpringJdbcTemplate 등

  • JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
  • 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트, 수동 플러시
  • 객체에 대한 작업이 일어나면 변경점은 아직 영속성 컨텍스트에 있는데 직접 커넥션을 사용하게되면 변경에 대한 결과를 얻기 위해서는 flush를 해주어야 함. 그래야 commit 후, 쿼리를 날리게 됨.

기본 문법과 쿼리 API

JPQL문법

select_문 :: = 
	select_절
	from_절
	[where_절]
	[groupby_절]
	[having_절]
	[orderby_절]
update_문 :: = update_절 [where_절] 
delete_문 :: = delete_절 [where_절]
  • select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분O (Member, age)
  • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
  • 엔티티 이름 사용, 테이블 이름이 아님(Member)별칭은 필수(m) (as는 생략가능)

집합과 정렬

select
	COUNT(m), //회원수
	SUM(m.age), //나이 합
	AVG(m.age), //평균 나이
	MAX(m.age), //최대 나이 
	MIN(m.age) //최소 나이
from Member m

TypeQuery, Query

  • TypeQuery: 반환 타입이 명확할 때 사용
  • Query: 반환 타입이 명확하지 않을 때 사용
// 리턴 타입을 받을 수 있음
TypedQuery<Member> memberTypedQuery = em.createQuery("select m from Member m",Member.class);
TypedQuery<String> memberTypedQuery2 = em.createQuery("select m.username from Member m",String.class);

// 리턴 타입이 명확하지 않을 때는 그냥 쿼리 객체를 이용해 결과 받음
Query emQuery = em.createQuery("select m.username, m.age from Member m",String.class);

결과조회 API

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException
// 리스트로 받기 - 여러개 또는 하나 있어야 함
TypedQuery<Member> memberTypedQuery3 = em.createQuery("select m from Member m",Member.class);
List<Member> memberList = memberTypedQuery3.getResultList();

// 싱글 객체로 받기 - 무조건 결과가 하나가 나와야 함
TypedQuery<Member> memberTypedQuery4 = em.createQuery("select m from Member m where m.id = 10L",Member.class);
Member member1 = memberTypedQuery4.getSingleResult();
// Spring Data Jpa -> 결과가 없어도 익셉션 안 나옴. (스프링이 트라이 캐치 한 번 해줌)

파라미터 바인딩

// 이름 기준으로 바인딩
SELECT m FROM Member m where m.username=:username 
query.setParameter("username", usernameParam);

// 위치 기준으로 바인딩
SELECT m FROM Member m where m.username=?1 
query.setParameter(1, usernameParam);
  • 위치 기준으로 바인딩을 사용하면 나중에 유지보수 힘들고 에러 발생의 원인이 됨.

프로젝션(select)

  • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
    • SELECT m FROM Member m -> 엔티티 프로젝션
    • SELECT m.team FROM Member m -> 엔티티 프로젝션
    • SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
    • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
  • DISTINCT로 중복 제거
  • 엔티티 프로젝션을 하면 결과들이 다 영속성으로 관리됨. → 변경하면 업데이트 쿼리를 날림.

여러 값 조회

SELECT m.username, m.age FROM Member m

  1. Query 타입으로 조회
List list = em.createQuery("select m.username, m.age from Member m").getResultList();
Object o = list.get(0);
Object[] result = (Object[]) o;
System.out.println("username -> " + result[0]);
System.out.println("age -> " + result[1]);
  1. Object[] 타입으로 조회
List<Object[]> list2 = em.createQuery("select m.username, m.age from Member m").getResultList();
Object[] result2 = list2.get(0);
System.out.println("username -> " + result2[0]);
System.out.println("age -> " + result2[1]);
  1. new 명령어로 조회

     List<MemberDTO> memberDTOList = em.createQuery("select new jpql.MemberDTO( m.username, m.age ) from Member m").getResultList();
     MemberDTO memberDTO = memberDTOList.get(0);
     System.out.println("username -> " + memberDTO.getUsername());
     System.out.println("age -> " + memberDTO.getAge());
    
  • 단순값을 DTO로 바로조회

      SELECT new jpabook.jpql.UserDTO(m.username, m.age)
      FROM Member m
    
  • 패키지명을포함한전체클래스명입력
  • 순서와 타입이 일치하는 생성자 필요

Paging(페이징)

  • JPA는 페이징을 다음 두 API로 추상화
    • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
    • setMaxResults(int maxResult) : 조회할 데이터 수
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
                    .setFirstResult(1) // 처음 어디부터 가져올 것인지?
                    .setMaxResults(10) // 몇개 까지 가져올 것인지?
                    .getResultList();
System.out.println("result = " + result.size());
for (Member m : result) {
	System.out.println("Member -> " + m.toString());
}

...
Hibernate: 
    /* select
        m 
    from
        Member m 
    order by
        m.age desc */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as TEAM_ID4_0_,
            member0_.username as username3_0_ 
        from
            Member member0_ 
        order by
            member0_.age desc limit ?
result = 10
Member -> Member{id=100, username='member99', age=99}
Member -> Member{id=99, username='member98', age=98}
Member -> Member{id=98, username='member97', age=97}
Member -> Member{id=97, username='member96', age=96}
Member -> Member{id=96, username='member95', age=95}
Member -> Member{id=95, username='member94', age=94}
Member -> Member{id=94, username='member93', age=93}
Member -> Member{id=93, username='member92', age=92}
Member -> Member{id=92, username='member91', age=91}
Member -> Member{id=91, username='member90', age=90}
  • MySQL 의 경우 페이징 쿼리 나가는 내용
SELECT
	M.ID AS ID
	,M.AGE AS AGE
	,M.TEAM_ID AS TEAM_ID
	,M.NAME AS NAME
FROM 
	MEMBER
ORDER BY
	MM.NAME DESC LIMIT ?, ?
  • Oracle의 경우 페이징 쿼리 내용
SELECT * FROM
	( SELECT ROW_.*, ROWNUM ROWNUM_
		FROM( 
			SELECT
				M.ID AS ID,
				M.AGE AS AGE,
				M.TEAM_ID AS TEAM_ID,
				M.NAME AS NAME
			FROM MEMBER M
			ORDER BY M.NAME
		) ROW_
	WHERE ROWNUM <= ?
	)
WHERE ROWNUM_ > ?

데이터베이스 방언에 따라 페이징 쿼리의 내용이 변경되어 조회됨

조인(Join)

  • 내부 조인:SELECT m FROM Member m [INNER] JOIN m.team t
  • 외부 조인:SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
    • 외부 조인 대상이 없어도 데이터 넣고 조회가 됨.
  • 세타 조인:select count(m) from Member m, Team t where m.username = t.name
    • 곱하기로 다 조회한 다음에 조건절의 조건에 따라 조회가 됨.

ON 절

  • ON절을 활용한 조인(JPA 2.1부터 지원)
    • 조인 대상 필터링
    • 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터) 1. 조인 대상 필터링

    예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

      -- JPQL
      SELECT m, t FROM Member m 
      LEFT JOIN m.team t **on** t.name = 'A'
        
      -- SQL
      SELECT m.*, t.* FROM Member m 
      LEFT JOIN Team t **ON** m.TEAM_ID=t.id and t.name='A'
    
  1. 연관관계 없는 엔티티 외부 조인

    예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

     -- JPQL
     SELECT m, t FROM Member m 
     LEFT JOIN Team t **on** m.username = t.name
        
     -- SQL
     SELECT m.*, t.* FROM Member m 
     LEFT JOIN Team t **ON** m.username = t.name
    

서브쿼리(Subquery)

  • 나이가 평균보다 많은 회원
select m from Member m
where m.age > **(select avg(m2.age) from Member m2)**
  • 한 건이라도 주문한 고객
select m from Member m
where **(select count(o) from Order o where m = o.member)** > 0

서브쿼리에서 지원하는 함수

  • [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
    • {ALLANYSOME} (subquery)
    • ALL 모두 만족하면 참
    • ANY,SOME 같은 의미, 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
  • 예제
    • 팀A 소속인 회원

      select m from Member m
      where **exists** (select t from m.team t where t.name = ‘팀A')
      
    • 전체 상품 각각의 재고보다 주문량이 많은 주문들

      select o from Order o
      where o.orderAmount > **ALL** (select p.stockAmount from Product p)
      
    • 어떤 팀이든 팀에 소속된 회원

      select m from Member m
      where m.team = **ANY** (select t from Team t)
      

서브쿼리의 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능(하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결
    • 애플리케이션 레벨로 올려서 해결
    • Native SQL 로 해결 등

** 하이버네이트6 변경 사항

하이버네이트6 부터는 FROM 절의 서브쿼리를 지원합니다.

참고링크 → https://in.relation.to/2022/06/24/hibernate-orm-61-features/

JPQL 타입

  • 문자: ‘HELLO’, ‘She’’s’
  • 숫자: 10L(Long), 10D(Double), 10F(Float)
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

JPQL 기타

  • SQL과 문법이 같은 식
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

조건식

CASE문

  • 기본 CASE 식
select
	case when m.age <= 10 then '학생요금'
			 when m.age >= 60 then '경로요금'
			 else '일반요금'
	end
from Member m
  • 단순 CASE 식
select
	case t.name when '팀A' then '인센티브110%' 
							 when '팀B' then '인센티브120%' 
							 else '인센티브105%'
	end
from Team t
  • COALESCE: 하나씩 조회해서 null이 아니면 반환
  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
    • 사용자 이름이 없으면 이름 없는 회원을 반환

        **select coalesce**(m.username,'이름 없는 회원') **from** Member m
      
    • 사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환

        **select NULLIF**(m.username, '관리자') **from** Member m
      

JPQL 기본함수

표준 함수 - 데이터베이스 상관 없이 사용 가능.

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX(JPA 용도)

사용자 정의 함수

  • 하이버네이트는 사용전 방언에 추가해야 한다.
    • 방언을 상속받는 클래스를 만들어 추가한다.

      package dialect;
          
      import org.hibernate.dialect.H2Dialect;
      import org.hibernate.dialect.function.StandardSQLFunction;
      import org.hibernate.type.StandardBasicTypes;
          
      public class MyH2Dialect extends H2Dialect {
          public MyH2Dialect() {
              // 함수 등록하기!
              registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
          }
      }
      
      ... <persistence.xml> ...
      ...
      <property name="hibernate.dialect" value="dialect.MyH2Dialect"/>
      ...
      ...
      
  • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
**select function**('group_concat', i.name) **from** Item i

19. 객체지향 쿼리 언어 - 중급 문법

경로표현식

  • .(점)을 찍어 객체 그래프를 탐색하는 것
select **m.username** -> 상태 필드
from Member m
join **m.team t** -> 단일  연관 필드

join **m.orders o** -> 컬렉션  연관 필드
where t.name = '팀A'
  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드 (ex: m.username)
  • 연관 필드(association field): 연관관계를 위한 필드
    • 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

특징

  • 상태 필드(state field): 경로 탐색의 끝, 탐색X
  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O
    • 묵시적 내부 조인이 발생하도록 쿼리를 짜면 안된다.

      // 멤버와 연관된 팀을 가져오겠다. -> 팀도 조인해야지 데이터베이스에서 SQL로 가져올 수 있음. (묵시적 내부 조인 발생)
      String query2 = "select m.team from Member m";
      
  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색X
    • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능

      // 컬렉션 값 연관 경로 -> 묵시적 내부 조인 발생 -> 더 이상 탐색 불가.
      // 컬렉션에서 어떤걸 가져와야할 지 모르기 때문에 ... 
      String query3 = "select t.memberList from Team t";
          
      Collection result = em.createQuery(query2, Collection.class).getResultList();
          
      // 이런식으로 명시적 조인을 해서 가져와야 함.
      String query4 = "select m.username from Team t join t.memberList m";
      

상태필드 경로 탐색

  • JPQL
select m.username, m.age from Member m
  • SQL
select m.username, m.age from Member m

단일 값 연관 경로 탐색

  • JPQL
select **o.member** from Order o
  • SQL
select m.* from Orders o **inner join Member m on o.member_id = m.id**

명시적/묵시적 조인

  • 명시적 조인: join 키워드 직접 사용
select m from Member m **join m.team t**
  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
select **m.team** from Member m

경로표현식 예제

select o.member.team from Order o -> 성공
select t.members from Team -> 성공
select t.members.username from Team t -> 실패
select m.username from Team t join t.members m -> 성공

경로 탐색을 사용한 묵시적 조인 시, 주의사항

  • 항상 내부 조인
  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌

실무에서는?

  • 가급적 묵시적 조인 대신에 명시적 조인 사용
  • 조인은 SQL 튜닝에 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

페치 조인(Fetch Join)

  • SQL 조인 종류X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용페치 조인 ::= [ LEFT [OUTER]INNER ] JOIN FETCH 조인경로

예시1

  • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에) SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT

[JPQL]

select m from Member m **join fetch** m.team

[SQL]

SELECT M.*, **T.*** FROM MEMBER M**INNER JOIN TEAM T** ON M.TEAM_ID=T.ID

엔티티 페치 조인

Untitled

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
												    .getResultList();
for (Member member : members) {
	//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
	System.out.println("username = " + member.getUsername() + ", " +
											 "teamName = " + **member.getTeam().name()**);
}
...
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B

조인 페치를 사용하지 않으면?

String query = "select m from Member m";
List<Member> memberList = em.createQuery(joinFetchQuery,Member.class).getResultList();

for(Member s : memberList) {
		System.out.println(s.toString() + " Team -> " + s.getTeam().getName());
    // 팀을 가져와야 하는 쿼리를 처음에 날리고 이후에는 캐싱되어 쿼리 안날리게 됨.
    // 회원 1 -> 팀A (SQL)
    // 회원 2 -> 팀A (1차 캐시)
    // 회원 3 -> 팀B (SQL)
    // 회원 4 -> 팀B (1차 캐시)
		//                Hibernate:
		//                select
		//                team0_.id as id1_3_0_,
		//                        team0_.name as name2_3_0_
		//                from
		//                Team team0_
		//                where
		//                team0_.id=?
		//                Member{id=3, username='회원1', age=18} Team -> teamA
		//                Member{id=4, username='회원2', age=18} Team -> teamA
		//                Hibernate:
		//                select
		//                team0_.id as id1_3_0_,
		//                        team0_.name as name2_3_0_
		//                from
		//                Team team0_
		//                where
		//                team0_.id=?
		//                Member{id=5, username='회원3', age=18} Team -> teamB
		//                Member{id=6, username='회원4', age=18} Team -> teamB
}

조인 페치를 사용하는 경우

String joinFetchQuery = "select m From Member m join fetch m.team";
List<Member> memberList = em.createQuery(joinFetchQuery,Member.class).getResultList();

for ... { ... } ...
// 조인 패치를 사용하는 경우 로그 -> 팀 가져오는게 프록시가 아님. 실제 데이터를 조회함.
                //    Hibernate:
                //    /* select
                //        m
                //    From
                //        Member m
                //    join
                //        fetch m.team */ select
                //                member0_.id as id1_0_0_,
                //                        team1_.id as id1_3_1_,
                //                member0_.age as age2_0_0_,
                //                        member0_.memberType as memberTy3_0_0_,
                //                member0_.TEAM_ID as TEAM_ID5_0_0_,
                //                        member0_.username as username4_0_0_,
                //                team1_.name as name2_3_1_
                //                        from
                //                Member member0_
                //                inner join
                //                Team team1_
                //                on member0_.TEAM_ID=team1_.id
                //                Member{id=3, username='회원1', age=18} Team -> teamA
                //                Member{id=4, username='회원2', age=18} Team -> teamA
                //                Member{id=5, username='회원3', age=18} Team -> teamB
                //                Member{id=6, username='회원4', age=18} Team -> teamB
  • 지연 로딩으로 세팅해도 항상 페치 조인이 먼저다.

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인

[JPQL]

select t
from Team t **join fetch t.members**
where t.name = ‘팀A'

[SQL]

SELECT T.*, **M.***FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

Untitled

// 컬레션 페치 조인
String query2 = "select t from Team t join fetch t.memberList where t.name = 'teamA'";
List<Team> memberList2 = em.createQuery(query2, Team.class).getResultList();

for (Team t : memberList2) {
	System.out.println(t.toString());
  for(Member member : t.getMemberList()) {
	  // DB에서 결과 조회 한 만큼 가져옴. 
    m.out.println("member -> " + member);
  }
}
...
Team{id=1, name='teamA'}
member -> Member{id=3, username='회원1', age=18}
member -> Member{id=4, username='회원2', age=18}
Team{id=1, name='teamA'}
member -> Member{id=3, username='회원1', age=18}
member -> Member{id=4, username='회원2', age=18}
...

DISTINCT 옵션

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    • SQL에 DISTINCT를 추가
    • 애플리케이션에서 엔티티 중복 제거
select **distinct** t
from Team t join fetch t.members
where t.name = 팀A

SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패

  • **DISTINCT가 추가로 애플리케이션에서 중복 제거시도**
  • **같은 식별자를 가진 Team 엔티티 제거**
String query3 = "select distinct t from Team t join fetch t.memberList where t.name = 'teamA'";
...
...
[DISTINCT 추가시 결과]
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300

** 하이버네이트6 변경 사항

DISTINCT가 추가로 애플리케이션에서 중복 제거시도 > 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.

참고 링크 : https://www.inflearn.com/questions/717679

일반 조인과 페치 조인의 차이점

  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음

[JPQL]

select t
from Team t join t.members m
where t.name = 팀A'

[SQL]

SELECT **T.***FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
  • JPQL은 결과를 반환할 때 연관관계 고려X
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
  • **페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)**
  • **페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념**

페치 조인의 한계

한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.(Alias)
    • 기본적으로 나랑 연관되는 애들을 다 가져오는 것이기 떄문에 → 이상하게 동작할 수 있음. (정합성 이슈)
    • JPA 기본 설계 개념인 객체 그래프를 전체 조회하는 것과 상반되는 행동이다.
    • 하이버네이트는 가능, 가급적 사용X
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult,setMaxResults)를 사용할 수 없다
    • 일대일, 다대일 같은 단일 값 연관필드들은 페치조인 해도 페이징가능
    • 일대다는 안됨.
      • 연관관계를 뒤집어서 해결
      • @BatchSize 옵션을 통해 연관된 데이터를 다 가져오도록 함.

          @BatchSize(size = 100)
          @OneToMany(mappedBy = "team")
          private List<Member> memberList = new ArrayList<>();
                
          // 원래 저 옵션이 없다면 
          **select t from Team t**
          // 위 쿼리를 날렸을 때 팀과 연관된 멤버를 찾는 콜이 멤버 수 만큼 나가야 함.
          // 옵션을 주면 한 번에 100개씩 날리기 때문에 모아서 1번만 나감.
        
        • 보통 해당 배치 사이즈 옵션은 글로벌 설정으로 많이 가져감

          <!-- persistence.xml -->
          <property name="hibernate.default_batch_fetch_size" value="100"/>
          
        • 이렇게 하면 쿼리가 n+1이 아니라, 가져오려는 테이블 수 많큼 맞출 수 있음.

      • DTO를 활용하여 new Operation을 통해 해결
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)

특징

  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

정리

  • 모든 것을 페치 조인으로 해결할 수 는 없음
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
    1. 엔티티를 페치 조인을 해서 전부 다 조회하여 그걸 그대로 쓴다.
    2. 페치 조인 한 엔티티를 애플리케이션에서 DTO로 변경해서 사용한다.
    3. 처음부터 JPQL 짤 때부터, new Operation으로 DTO로 스위칭해서 가져온다.
  • 성능 튜닝에 연관이 많다.

다형성 쿼리

TYPE

조회 대상을 특정 자식으로 한정 예) Item 중에 Book, Movie를 조회해라

[JPQL]

select i from Item i
where **type(i)** IN (Book, Movie)

[SQL]

select i from i
where i.DTYPE in (B, M)

TREAT(jpa 2.1)

  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용

엔티티 직접사용

기본 키 값

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용

[JPQL]

select **count(m.id)** from Member m //엔티티의 아이디를 사용
select **count(m)** from Member m //엔티티를 직접 사용

[SQL](JPQL 둘다 같은 다음 SQL 실행)

select count(m.id) as cnt from Member m
  • 엔티티를 파라미터로 전달
String jpql = select m from Member m where **m = :member**;
List resultList = em.createQuery(jpql)
.setParameter("member", **member**)
.getResultList();
  • 식별자를 직접 전달
String jpql = select m from Member m where **m.id = :memberId**;
List resultList = em.createQuery(jpql)
.setParameter("memberId", **memberId**)
.getResultList();
  • 실행된 SQL
select m.* from Member m where **m.id=?**

외래 키 값

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 외래 키 값을 사용히여 조회한다.

Team **team** = em.find(Team.class, 1L);
// 멤버의 데이터인 TEAM_ID를 이용하여 조회
String qlString = select m from Member m where **m.team = :team**;
List resultList = em.createQuery(qlString)
.setParameter("team", **team**)
.getResultList();

// TEAM_ID를 직접 주어도 상관 없음.
String qlString = select m from Member m where **m.team.id = :teamId**;
List resultList = em.createQuery(qlString)
.setParameter("teamId", **teamId**)
.getResultList();
  • 실행된 SQL
select m.* from Member m where **m.team_id**=?

NAMED 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증(*)**

엔티티에 직접 정의

@Entity
@NamedQuery(
	name = "Member.findByUsername" 
	,query="select m from Member m where m.username = :username"
)
public class Member {
...
}

List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
	.setParameter("username", "회원1").getResultList();

XML에 정의

  • [META-INF/persistence.xml]
<persistence-unit name="jpabook">
	<mapping-file>META-INF/ormMember.xml</mapping-file>
  • [META-INF/ormMember.xml]
**<?xml version="1.0" encoding="UTF-8"?>**
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
	<named-query name="Member.findByUsername">
		<query>**<![CDATA[
			select m
			from Member m
			where m.username = :username
		]]>**</query>
	</named-query>
	<named-query name="Member.count">
		<query>select count(m) from Member m</query>
	</named-query>
</entity-mappings>

NAME 쿼리 환경에 따른 설정

  • XML이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
  • SpringDataJpa 에서는 인터페이스에 바로 선언이 가능하기 때문에, JpaRepository를 상속받고 그 내부에서 쿼리를 선언해 놓으면 애플리케이션 로딩 시점에서 전부 검증해준다. 결국, SpringDataJpa를 사용하는 경우 전부 NAMED 쿼리로 등록이 된다.

Bulk 연산

  • 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
  • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
    • 재고가 10개 미만인 상품을 조회한다.
    • 상품 엔티티의 가격을 10% 증가시킨다.
    • 트랜잭션 커밋 시점에 변경이 감지되어 동작한다.
  • 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

예제

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원INSERT(insert into .. select, 하이버네이트 지원)
**String qlString = "update Product p " +
                  "set p.price = p.price * 1.1 " +
                  "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(qlString)
                    .setParameter("stockAmount", 10)
                    .executeUpdate();**

벌크 연산 주의 사항

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리하기 때문에 주의!
    1. 벌크 연산을 먼저 실행(영속성 컨텍스트에 아무것도 하지말고)
    2. 벌크 연산 수행 후 영속성 컨텍스트 초기화
      ...
      // 팀과 멤버 persist();
      ...
      // 영속성 컨텍스트 초기화 안해도, 밑에 벌크가 나중에 나감.
      //            em.flush();
      //            em.clear();
        
      // 1. FLUSH 자동 호출. 커밋,쿼리에서 자동으로 호출함.
      int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate();
      System.out.println(resultCount);
        
      // 4. 따라서, 영속성 컨텍스트를 초기화 후 작업해야 한다.
      em.clear();
        
      // 2. 다시 가져와도 벌크 연산의 결과는 조회되지 않는다.
      Member findMember = em.find(Member.class, member1.getId());
      System.out.println(findMember);
        
      // 3. 밑에 애들은 영속성 컨텍스트에 남아있기 때문에 20으로 업데이트 전인 값으로 조회 됨.
      System.out.println("member1.getAge() = " + member1.getAge());
      System.out.println("member2.getAge() = " + member2.getAge());
      System.out.println("member3.getAge() = " + member3.getAge());
    
  • SpringDataJpa 에서는 Modifying Query 에서 BulkOperation 을 사용한다.

© 2024. Chiptune93 All rights reserved.