자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

스프링에서 JPA 사용하면 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해준다.

컨테이너 환경에서 JPA 동작하는 내부 동작 방식을 이해해야 문제 발생 해결할 있다.

 

트랜잭션 범위의 영속성 컨텍스트

스프링 컨테이너의 기본 전략

트랜잭션 범위의 영속성 컨텍스트 전략이 기본

영속성 컨텍스트 생존 범위 == 트랜잭션 범위

 

트랜잭션 안에서 런타임 예외 발생으로 롤백 트랜잭션은 커밋을 하지 않는다.

이때 영속성 컨텍스트도 플러시가 되지 않아 DB 변경사항이 반영되지 않는다.

 

  • 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
    엔티티 매니저가 달라도 같은 영속성 컨텍스트를 사용한다
  • 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다
    같은
    엔티티 매니저를 사용해도 트랜잭션이 다르면 영속성 컨텍스트도 다르다

 

준영속 상태와 지연 로딩

대부분 서비스 계층에서 트랜잭션을 시작하므로 컨트롤러 계층에서는 엔티티는 준영속 상태가 된다.

따라서 지연로딩 전략을 취하면, 컨트롤러에서 처음으로 준영속 상태 엔티티 값을 꺼낼 예외가 발생하게 된다.

지연로딩이 아닌 준영속 엔티티를 컨트롤러에서 조작을 해도 변경감지 기능이 동작하지 않는다.

 

준영속 상태 지연 로딩 문제 해결법

  • 엔티티를 미리 로딩하기
  • OSIV 사용해서 엔티티를 영속 상태로 유지하기

 

엔티티 미리 로딩하기

  • 글로벌 패치 전략 변경
  • JPQL 페치 조인
  • 강제로 초기화

 

글로벌 패치 전략 즉시 로딩 단점

  • 사용하지 않는 엔티티를 로딩
  • N+1 문제가 발생

직접이던, 스프링 데이터 JPA 생성해 주던 JPQL 조회를 엔티티를 결과로 받는다면, 즉시로딩의 경우 연관된 엔티티를 전부 조회하게 된다.

 

 

 

 

 

예제의 경우 Member 5, Product 5번의 추가 쿼리가 수행된다.

 

 

JPQL 페치 조인

글로벌 페치 조인은 애플리케이션 전역에 영향을 주기에 적절한 방법이 아니다.

 

페치 조인을 하게 되면 N+1 문제도 발생하지 않는다.

조인을 사용하기 때문에 SQL 번으로 조회하기 때문이다.

N+1 문제를 해결할 가장 현실적인 방법

 

 

 

JPQL 페치 조인 단점

가장 간편하고 현실적인 N+1 해결 방법이기에 너무 무분별하게 사용할 수가 있다.

프리젠테이션 계층에서 데이터를 보여줄 목적으로 페치조인을 사용한다는 것부터가 프리젠테이션 계층이 데이터 접근 계층에 직접적으로 의존하게 된다.

보여줄 화면 마다 페치 조인을 사용하게 된다면, 리포지토리에 많은 페치조인 메소드가 증가할 있다.

 

강제로 초기화

프리젠테이션 계층에 넘기기 전에 프록시 객체를 강제로 초기화하는 것을 말한다.

말은 거창하지만, 프록시 객체에 어느 메서드나 호출하면 초기화가 이루어지므로 아무 게터 메서드나 호출한 엔티티를 반환하면 된다.

 

프리젠테이션에서 보여줄 데이터 때문에 이렇게 엔티티를 미리 로딩한다는 것은 서비스 계층이 프리젠테이션 계층에 의존하는 것과 같다. 프리젠테이션 계층의 변화가 서비스 계층에 영향을 미치게 된다.

그래서 사이에 FACADE 계층을 추가해 일을 위임한다.

 

FACADE 계층

프리젠테이션 계층과 서비스 계층 사이에 추가 계층을 두어 뷰를 위한 프록시 초기화만 담당한다.

서비스 계층은 이상 뷰에서 사용할 프록시 객체를 초기화하지 않아도 된다. , 의존성이 제거된다.

 

 

준영속 상태와 지연 로딩의 문제점

뷰를 개발할 때마다 필요한 엔티티를 초기화하는 것은 오류가 발생할 가능성이 높다.

아무리 중간에 FACADE 같은 별도의 계층으로 처리를 한다고 해도 초기화를 빼먹을 수도 있다.

또한 FACADE 코드 레벨의 물리적인 의존성은 제거했지만, 논리적인 의존성을 제거하지 못한다. , 완전한 해결책이 되지 못한다.

 

초기화 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 뷰에서도 지연 로딩을 가능하게 있는데 이를 OSIV 한다

 

OSIV

Open Session In View

여기서 세션은 영속성 컨텍스트를 의미한다.

OSIV 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다.

 

OSIV 요청 트랜잭션

요청이 들어오면, 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서 트랜잭션을 시작하고 요청이 끝나면 트랜잭션과 영속성 컨텍스트를 종료한다.

 

요청 트랜잭션 방식 OSIV 문제점

컨트롤러나 뷰에서 엔티티를 변경할 있다.

예를 들어 뷰에서 개인정보를 마스킹해서 보여줘야해서 컨트롤러 계층에서 엔티티에 마스킹 처리를 하면, 실제로 마스킹된 결과가 DB 반영된다.

 

프리젠테이션 계층에서 엔티티를 수정못하게 하는 방법

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 레핑
  • DTO 반환

 

엔티티를 읽기 전용 인터페이스로 제공

 

 

 

엔티티 래핑

읽기 메서드만 있는 메서드를 정의한 클래스로 엔티티를 감싼다.

 

 

DTO 반환

엔티티 대신 별도 데이터 전용 DTO 정의하고, DTO 생성해 반환한다.

 

 

가지 방법은 전부 코드량이 증가하고, 번거롭다는 단점이 있다.

그래서 요청 트랜잭션 방식 OSIV 사용하지 않는다.

 

스프링 OSIV: 비즈니스 계층 트랜잭션

스프링 프레임워크가 제공하는 OSIV

 

스프링 프레임워크가 제공하는 OSIV 라이브러리

서블릿 필터, 스프링 인터셉터 어디서 적용할지에 따라 필요한 클래스가 달라진다.

  • 하이버네이트
  • 스프링

OSIV 하이버네이트에서 부르는 명칭

JPA 정식 표현은 OEIV

OSIV == OEIV 같은 뜻이다.

 

스프링 OSIV 분석

요청 트랜잭션 방식의 OSIV 프리젠테이션 계층에서 데이터 변경을 막을 없다는 치명적인 문제가 있다.

스프링 프레임워크가 제공하는 OSIV 문제를 어느 정도 해결했다.

 

  • 요청이 들어오면 설정한 클래스에 따라 필터나, 인터셉터에서 영속성 컨텍스트를 생성한다.
  • 서비스 계층에서 생성된 영속성 컨텍스트에 트랜잭션 시작한다.
    @Transactional
    붙은 클래스에 한함
  • 서비스 계층 작업이 끝나면, 트랜잭션을 커밋하고, 영속성 컨텍스트를 플러시 한다.
  • 필터나, 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 하지 않는다.

최종적으로 프리젠테이션 계층인 컨트롤러, 뷰에서 영속성 컨텍스트가 살아있고, 이후 영속성 컨텍스트가 소멸될 플러시를 호출하지 않으므로 지연로딩 문제와 엔티티 수정 문제에서 자유로워 진다.

 

트랜잭션 없이 읽기

영속성 컨텍스트 모든 변경 사항은 트랜잭션 안에서 이뤄져야 한다.

트랜잭션 종료 플러시를 하면 jakarta.persistence.TransactionRequiredException 예외가 발생한다.

 

만약 엔티티 변경이 없이 단순히 조회만 한다면 트랜잭션이 필요없다. 이를 트랜잭션 없이 읽기라 한다.

 

스프링이 제공하는 OSIV 영속성 컨텍스트가 프리젠테이션 계층까지 유지된다. , 트랜잭션은 종료된 상태다. , 트랜잭션 없이 읽기가 가능한 상태다.

 

강제로 플러시를 해봤자 예외만 발생한다.

 

스프링 OSIV 주의사항

서비스 계층를 통해 조회해온 엔티티를 수정하고, 다른 서비스 계층 레이어를 조회하면, 문제가 된다.

영속성 컨텍스트는 살아 있고, 트랜잭션이 종료됐는데 다시 트랜잭션이 시작된다. 다시 시작된 트랜잭션이 종료될 , 커밋이 이루어지며 플러시가 발생한다. 이전에 안전하다고 생각했던 엔티티 변경 사항이 DB 반영된다.

 

일반적인 해결책은 트랜잭션이 걸린 비즈니스 로직을 모두 호출 뷰에 보여줄 엔티티를 수정하는 것이다.

 

스프링 OSIV 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 있어 이런 문제가 발생한다.

 

 

OSIV 정리

  • 스프링 OSIV 특징
    영속성
    컨텍스트는 요청 시작부터 종료까지 유지된다.
    이때 트랜잭션은 @Transactional 붙은 서비스 레이어를 호출할 시작하고, 종료된다. , 생명주기가 짧다.
    영속성 컨텍스트 종료 플러시를 호출하지 않으므로 프리젠테이션 레이어에서 영속 상태 엔티티를 수정해도 DB 반영되지 않는다.

 

  • 스프링 OSIV 단점
    하나의
    영속성 컨텍스트를 여러 트랜잭션이 공유할 있다.
    프리젠테이션 계층에서 의도하지 않은 플러시가 발생할 있다.

 

  • 그럼에도 OSIV
    FACADE, DTO
    등을 사용하면 결국 신규로 작성해야하는 코드가 많아진다.
    프록시의 경우 강제로 초기화해서 넘겨야하는데 빼먹는 다면 예외가 발생한다.

 

  • OSIV 만능이 아니다
    프리젠테이션
    계층에서 출력할 엔티티를 자유롭게 사용할 있지만, 복잡한 뷰에선 엔티티 조회가 아닌 통계를 위한 별도 JPQL이나 SQL 조회하는 것이 효과적이다. 이땐 DTO 사용하게 된다

 

  • OSIV 같은 JVM 벗어난 원격 상황에서는 사용할 없다
    외부에서
    호출하는 클라이언트의 경우 결국 JSON이나 XML 전송해야한다.
    경우 엔티티를 JSON 시켜 외부 API 노출해야 한다.
    내부 API 경우 클라이언트 - 서버를 전부 제어가능하지만 외부 API 불가능하다
    그래서
    엔티티 정의를 바꾸기 어려워 진다.

 

엔티티는 생각보다 자주 변경되므로 외부 API 경우 엔티티를 직접 JSON으로 변환시키보다 중간 완충제로 DTO 두어 노출하는 것이 안전하다.

 

너무 엄격한 계층

OSIV 사용하기 전엔 프리젠테이션 계층에서 지연 로딩된 엔티티를 여러 방법을 통해 미리 초기화해야 했다.

OSIV 사용하면 영속성 컨텍스트가 프리젠테이션까지 살아있으므로 지연 로딩 엔티티를 초기화할 필요가 없다. 따라서 단순 조회용 엔티티의 경우 굳이 서비스 레이어를 거치지 않고 리포지토리에 직접 접근해 엔티티를 조회해도 문제가 없다.

 

정리

스프링 JPA 사용하면 트랜잭션 범위의 영속성 컨텍스트 전략이 기본 적용된다.

같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.

전략의 단점은 프리젠테이션 계층에서 준영속 엔티티를 다루게 되어 지연 로딩 엔티티를 사용하지 못한다.

OSIV 이런 문제를 해결할 있다.

 

 

 

'개발 > JPA' 카테고리의 다른 글

12 스프링 데이터 JPA  (1) 2024.06.03
10 객체지향 쿼리 언어  (0) 2024.05.27
09 값 타입  (0) 2024.05.20
08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

스프링 데이터 JPA 소개

리포지토리 계층의 반복되는 DB 데이터 접근과 CRUD 코드를 해결해준다.

 

인터페이스만 작성하면 애플리케이션 실행 시점에 스프링 데이터 JPA 구현 객체를 동적으로 생성해준다.

 

적절한 타입 파라미터만 넣어주면 된다.

 

JpaRepository 인터페이스에 없는 메서드도 스프링 데이터 JPA 메소드 이름을 분석해 JPQL 생성해준다.

 

스프링 데이터 프로젝트

스프링 데이터 프로젝트는 다양한 데이터 저장소에 대한 접근을 추상화했다

스프링 데이터 JPA JPA 특화된 기능을 제공한다.

 

공통 인터페이스 기능

 

 

 

 

 

가장 간단한 방법은 JpaRepository를 구현하면된다.

T 반환 엔티티 타입, ID 식별자 타입이다.

 

쿼리 메소드 기능

메소드 이름만으로 쿼리를 생성하는 기능

인터페이스에 메소드만 선언하면, 메소드 이름 기반으로 JPQL 쿼리가 생성되고 실행된다.

 

메소드 이름으로 쿼리 생성

 

Keyword Sample JPQL snippet
Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
IsEquals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNullNull findByAge(Is)Null … where x.age is null
IsNotNullNotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

Table 1. Supported keywords inside method names

 

출처: <https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html>

 

JPA NamedQuery

xml이나 애노테이션으로 쿼리를 정의

 

이렇게 호출하면 된다.

스프링 데이터 JPA "도메인클래스.메소드이름"으로 쿼리를 찾아 실행한다.

@Param 이름기반 파라미터를 바인딩하는 애노테이션이다

 

JPA 직접 사용했다면, 아래와 같이 사용한다.

 

@Query 리포지토리 메소드에 쿼리 정의

JPQL

JPQL 1부터 시작

Native SQL

0부터 시작한다.

 

파라미터 바인딩

이름 기반, 위치 기반 모두 지원하지만 가급적 이름 기반 파라미터를 사용하는 것이 좋다.

 

벌크성 수정 쿼리

 

 

반환 타입

메서드 이름은 그대로 두고, 반환 타입만으로 유연하게 설정할 있다.

List<Entity> 이상, Entity

0 컬렉션, null 각각 반환한다.

, 건을 기대하고 반환타입을 지정했는데 이상인 경우는 예외가 발생한다.

NonUniqueResultException

 

페이징과 정렬

https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.special-parameters

 

Page<User> findByLastname(String lastname, Pageable pageable);

 

Slice<User> findByLastname(String lastname, Pageable pageable);

 

List<User> findByLastname(String lastname, Sort sort);

 

List<User> findByLastname(String lastname, Sort sort, Limit limit);

 

List<User> findByLastname(String lastname, Pageable pageable);

 

힌트

주의할 것은 SQL 힌트가 아닌 JPA에게 힌트를 주는

'개발 > JPA' 카테고리의 다른 글

13 웹 애플리케이션과 영속성 관리  (1) 2024.06.10
10 객체지향 쿼리 언어  (0) 2024.05.27
09 값 타입  (0) 2024.05.20
08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

객체지향 쿼리

EntityManager.find() 메서드로 식별자를 통한 엔티티 검색은 한계가 있다.

복잡한 조회조건으로 검색을 있어야 한다.

 

JPQL 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.

벤더별 조금씩 다른 SQL 추상화하여 SQL 의존적이지 않다.

 

SQL DB Table 대상 쿼리

JPQL 엔티티 객체 대상 쿼리

JPQL 받은 JPA 적절한 SQL 변환한다.

 

JPA 지원하는 검색 방법

  • JPQL (Java Persistence Query Language)
  • Criteria 쿼리
    JPQL 쉽게 다루도록 도와주는 빌더 클래스 모음
  • 네이티브 SQL
  • QueryDSL
    JPQL
    쉽게 다루도록 도와주는 빌더 클래스 모음
  • JDBC 직접 사용, Mybatis 같은 SQL 매퍼 프래임워크 사용

 

핵심은 JPQL 이다. 이를 이해해야 이를 이용한 빌더 클래스도 이해할 있다.

 

 

JPQL 소개

엔티티 객체를 조회하는 객체지향 쿼리

SQL 문법이 매우 비슷

SQL 한번 추상화해 벤더에 종속적이지 않다.

JPQL 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 간결하다.

 



@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @Column(name = "username")
        private String name;
}

 

 

 

 

Criteria 쿼리 소개

JPQL 생성하는 빌더 클래스

 

가장 장점은 문자열로된 JPQL 대신 프로그래밍 코드로 JPQL 작성할 있다.

 

프로그래밍 코드 장점

  • 컴파일 시점에 오류 발견
  • IDE 도움으로 코스 추천 받을 있음
  • 동적 쿼리 작성 편리

 

JPA 기본 내장되어 있다.

 

 

보는 바와 같이 Criteria 코드로 JPQL 생성할 있지만, 장점을 무시할 정도로 지나치게 복잡하다.

 

QueryDSL 소개

JPQL 빌더

단순하고 사용하기 쉽다.

다만, JPA 내장되어 있지는 않다. 따라서 의존성 설정이 필요하다.

 



<?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>jpa-basic</groupId>
    <artifactId>jpa-01</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <hiberate.version>6.0.0.Final</hiberate.version>
        <querydsl.version>5.0.0</querydsl.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hiberate.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-hikaricp</artifactId>
            <version>${hiberate.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>23.3.0.23.09</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.21.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-apt -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <processor>
                                com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

 

 

 

 

바로 Maven generate-sources 눌러도 되나 Maven clean 돌리는 것을 권장

 

 

 

모르는 사람이 봐도 흐름이 이해가 만큼 쉽다.

 

네이티브 SQL 소개

 

 

직접 SQL 다루기 때문에 벤더 의존성이 생긴다. 바꿔 말하면, JPQL에서 표준화되어 지원하지 않고, 벤더에 의존하는 기능을 사용할 때는 유용할 있다.

 

JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용

JPA JDBC 커넥션을 획득하는 API 제공하지 않는다. JPA구현체가 제공하는 방법을 사용해야 한다.

위는 직접 JDBC 커넥션 얻는 , 아래는 하이버네이트가 JDBC 커넥션을 얻는

 

JDBC 마이바티스 같은 SQL 매퍼 프레임워크는 JPA 우회해서 DB 접근한다.

따라서 JPA 함께 사용하는 경우 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.

최악의 경우 영속성 컨텍스트와 DB 데이터 불일치로 무결성이 깨질 수가 있다.

 

이런 이슈를 해결하기 위해 JPA 우회하는 경우 SQL 실행 직전 영속성 컨텍스트를 수동으로 플러시해서 DB 영속성 컨텍스트를 동기화시켜야 한다.

 

스프링 프레임워크를 사용하면 JPA 마이바티스를 쉽게 통합할 있다.

AOP 활용하면 JPA 우회하는 모든 메서드에 강제로 플러시하는 코드를 넣을 수도 있다.

 

JPQL

Java Persistence Query Language

 

예제



@Entity
@Data
@ToString(exclude = {"orders"})
public class Member {
        @Id
        @GeneratedValue
        private Long id;
        
        @Column(name = "name")
        private String username;
        private int age;
        
        @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<Order> orders = new ArrayList<>();
        
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "team_id")
        private Team team;
        
        public void setTeam(Team team) {
                if (this.team != null) {
                        this.team.getMembers().remove(this);
                }
                this.team = team;
                team.getMembers().add(this);
        }
        
        public void addOrder(Order order) {
                orders.add(order);
                order.setMember(this);
        }
}
@Entity
@Data
public class Team {
        @Id
        @GeneratedValue
        private Long id;
        
        private String name;
        
        @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<Member> members = new ArrayList<>();
        
        public void addMember(Member member) {
                members.add(member);
                member.setTeam(this);
        }
}


@Entity
@Table(name = "Orders")
@Data
public class Order {
        @Id
        @GeneratedValue
        private Long id;
        
        private int orderAmount;
        
        @Embedded
        private Address address;
        
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        @ManyToOne(cascade = CascadeType.ALL)
        @JoinColumn(name = "product_id")
        private Product product;
        
        public void setMember(Member member) {
                if (this.member != null) {
                        this.member.getOrders().remove(this);
                }
                this.member = member;
                member.getOrders().add(this);
        }
}
@Entity
@Data
public class Product {
        @Id
        @GeneratedValue
        private Long id;
        
        private String name;
        private int price;
        private int stockAmount;
}
@Embeddable
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {
        private String city;
        private String street;
        private String zipcode;
}


public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                save();
                emf.close();
        }




        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setAge(20);
                        m1.setUsername("홍길동");
                        
                        Member m2 = new Member();
                        m2.setAge(30);
                        m2.setUsername("임꺽정");
                        
                        Member m3 = new Member();
                        m3.setAge(40);
                        m3.setUsername("유관순");
                        
                        Team t1 = new Team();
                        t1.setName("레드팀");
                        t1.addMember(m1);
                        t1.addMember(m2);
                        
                        em.persist(t1);
                        
                        Team t2 = new Team();
                        t2.setName("블루팀");
                        t2.addMember(m2);
                        t2.addMember(m3);
                        
                        em.persist(t2);
                        
                        Product p1 = new Product();
                        p1.setName("삼겹살");
                        p1.setPrice(10000);
                        p1.setStockAmount(10);
                        em.persist(p1);
                        
                        Product p2 = new Product();
                        p2.setName("오징어");
                        p2.setPrice(15000);
                        p2.setStockAmount(20);
                        em.persist(p2);
                        
                        Product p3 = new Product();
                        p3.setName("돈까스");
                        p3.setPrice(12000);
                        p3.setStockAmount(30);
                        em.persist(p3);
                        
                        Order o1 = new Order();
                        o1.setAddress(new Address("서울시", "을지로", "12345"));
                        o1.setMember(m1);
                        o1.setOrderAmount(1);
                        o1.setProduct(p1);
                        em.persist(o1);
                        
                        Order o2 = new Order();
                        o2.setAddress(new Address("서울시", "종로", "54321"));
                        o2.setMember(m1);
                        o2.setOrderAmount(1);
                        o2.setProduct(p2);
                        em.persist(o2);
                        
                        Order o3 = new Order();
                        o3.setAddress(new Address("서울시", "종로", "54321"));
                        o3.setMember(m2);
                        o3.setOrderAmount(1);
                        o3.setProduct(p2);
                        em.persist(o3);
                        
                        Order o4 = new Order();
                        o4.setAddress(new Address("서울시", "종로", "54321"));
                        o4.setMember(m3);
                        o4.setOrderAmount(1);
                        o4.setProduct(p2);
                        em.persist(o4);
                        
                        Order o5 = new Order();
                        o5.setAddress(new Address("서울시", "통일로", "55555"));
                        o5.setMember(m3);
                        o5.setOrderAmount(1);
                        o5.setProduct(p3);
                        em.persist(o5);
                });
        }


        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        logic.accept(em);
                        
                        tx.commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        tx.rollback();
                } finally {
                        em.close();
                }
        }
}

Order 는 예약어로 Orders 변경했다.

 

기본 문법과 쿼리 API

SELECT, UPDATE, DELETE 문이 있다.

INSERT 문은 엔티티를 만들어 EntityManager.persist()메서드를 호출하므로 없다.

JPQL에서 UPDATE, DELETE 문은 벌크 연산이라고 한다.

 

SELECT

  • 대소문자 구분
    SQL 키워드를 제외하고, 엔티티와 필드는 구분한다.
  • 엔티티 이름
    클래스
    명이 아니라 엔티티 , 컬럼 명이 아니라 필드 명이다.
  • 별칭 필수

 

TypeQuery, Query

반환 타입이 명확하면, TypeQuery 아니면 Query

 

 

 

 

 

 

 

 

결과 조회 메서드

getSingleResult() 경우 명확히 건이라고 명시했기 때문에 결과가 없거나 이상이면 예외가 발생한다.

 

파라미터 바인딩

이름 기준 파라미터(Named parameters)

 

 

 

위치 기준 파라미터

 

 

이름 기준 파라미터를 사용할 것을 권장한다.

위치는 순서에 의존하게 되고, 이름은 순서에 의존하지 않으며, 똑같은 파라미터를 여러 사용해도 번만 setParameter 호출하면 된다.

 

바인드 변수는 필수로 사용해야 한다.

직접 문자열로 덧붙여가며 SQL 만들면 SQL 인젝션 공격에 취약해진다.

바인드 변수를 사용할 경우, DB 쿼리를 파싱해 이를 재사용한다.


프로젝션

select , 조회할 컬럼을 지정하는 것을 projection 이라 한다.

JPQL 에서 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(기본형) 있다.

 

엔티티를 대상으로 조회를 하면, 조회된 엔티티는 영속성 컨텍스트가 관리한다.

 

임베디드 타입을 프로젝션으로 사용할 있다. , 엔티티가 아니라 DB 기준에선 단순한 컬럼들 묶음이기에 from 절에는 없다.

엔티티가 아니기 때문에 영속성 컨텍스트에 관리 대상이 아니다.

 

 

 

스칼라 타입은 단순히 기본형 값을 늘어놓는

 

객체 변환 작업을 스칼라 타입으로 직접 받을 있다.

 

다만, 제약 조건으로 전체 경로를 포함한 클래스 이름이 필요하다.

 

 

페이징 API

페이징도 반복적이고 지루한 작업이다. 이를 간단한 설정으로 처리할 있다.

 

 

페이징 처리 방식은 방언에 따라 벤더별로 알아서 처리된다.

 

 

집합과 정렬

GROUP BY, HAVING

 

 

 

통계 쿼리는 실시간 처리하기에 부담이 많아 통계 결과만 저장하는 테이블을 별도로 만들어 주고 새벽에 통계 쿼리를 생성하는 것을 권장

 

정렬(ORDER BY)

JPQL 조인

내부 조인

inner 키워드 생략가능

연관된 Member.Team 엔티티를 조인 조건으로 사용한다.

따로 ON 필요가 없다.

 

 

외부 조인

outer 키워드는 생략가능

 

 

컬렉션 조인

일대다, 다대일 관계의 조인을 말한다. , 컬렉션이 사용된다

 

 

 

 

세타 조인

where 절을 이용한 조인으로 내부 조인만 지원한다.

 

 

 

 

JOIN ON

JPQL 에서 ON 절은 JPA2.1 부터 지원한다.

내부 조인에서 ON 절은 Where 절과 같으므로, 보통 외부 조인 시에만 사용한다.

 

 

 

페치 조인

sql 조인 종류는 아님

JPQL 에서 성능 최적화를 위해 제공하는 기능

 

연관된 엔티티를 지연 조회를 하는 경우가 많은데

이를 무시하고 번에 조회할 사용

 

 

페치가 없는 경우

 

 

toString() 위해 Member.team 관련된 레드팀, 블루팀으로 번의 추가 쿼리가 실행됐다.

 

페치 조인으로 조회된 연관된 엔티티는 프록시가 아닌 실제 엔티티다.

따라서 영속성 컨텍스트에서 분리되도 연관된 엔티티를 조회할 있다.

 

컬렉션 페치 조인

 

 

 

페치 조인으로 인해 지연 로딩이 무시되고 즉시 조회가 됐다.

더불어 책에서는 id=2 결과가 중복으로 나와 아래와 같이 중복이 발생한다.

이렇게 중복이 나는 조건은 일대다 fetch 조인.

다만 최신 버전에서는 중복이 자동으로 잡히는 듯하다. 따라서 다음 DISTINCT 필요하지 않을 듯하다.

 

 

페치 조인과 DISTINCT

JPQL 에서 DISTINCT 명령어는 SQL 에서 DISTINCT 수행하는 것과 더불어

어플리케이션에서도 중복 제거를 수행한다.

 

 

 

 

페치 조인과 일반 조인 차이

JPQL 결과를 반환할 연관관계까지 고려하지 않는다. SELECT 절에 지정한 엔티티만 조회한다.

따라서 연관관계 엔티티는 프록시 객체다. 실제 사용 조회가 된다.

 

fetch 키워드가 붙으면, 어떤 설정도 무시하고 무조건 즉시 로딩으로 조회된다.

따라서 연관관계 엔티티도 실제 엔티티다.

 

페치 조인의 특징과 한계

페치 조인은 번에 연관된 엔티티를 조회해서 SQL 질의 수를 줄여 성능 최적화를 있다.

또한 글로벌 패치 전략을 무시하고 즉시 조회를 있다.

따라서 이를 조합해 글로벌 패치 전략은 LAZY 도배를 하고, 필요할 Fetch 조인을 사용하는 것이 좋다.

즉시 조회기 때문에 영속성 컨텍스트가 닫이거나 준영속 상태가 되도 객체 그래프 탐색을 있다.

 

패치 조인 한계

  • 페치 조인 대상은 별칭을 없다.
    , 별칭을 통해 where select 등에서 컬럼을 사용할 없다.
    다만, 하이버네이트는 별칭을 지원한다.
    별칭을 잘못 사용하면, 연관된 데이터 무결성이 깨질 있어 조심해야 한다.
  • 이상 컬렉션을 페치할 없다.
    컬렉션 * 컬렉션 카테시안 곱이 발생하기 때문에 예외가 발생한다.
    구현체에 따라 되기도 하지만 사용하지 않는 것이 맞다.
  • 컬렉션 페치 조인을 하면 페이징 API 사용할 없다.
    컬렉션이 아닌 일대일, 다대일들은 페치 조인을 해도 페이징 API 사용할 있다.
    하이버네이트 기준 컬렉션 페치 조인 페이징 API 사용하면
    경로
    로그와 함께 주메모리에서 페이징 처리를 한다.
    잘못하면 메모리 초과로 심각한 에러가 발생할 있어 위험하다

 

페치 조인을 남용하지 말고, 여러 테이블에서 연관된 데이터를 취합해야 한다면, 별로 DTO 만들어 변환하는 것도 고려한다.

 

경로 표현식

JPQL 에서 "."()으로 접근하는 것을 의미한다.

코드에서 탐색이 아니다.

묵시적 조인이 일어난다.

경로 표현식의 용어 정리

  • 상태 필드
  • 연관 필드

 

 

상태 필드 경로 탐색

 

 

단일 연관 경로 탐색

 

 

 

경로 표현식에 의해 묵시적으로 일어나는 조인은 무조건 내부 조인이다.

 

 

 

컬렉션 연관 경로 탐색

 

 

 

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

  • 항상 내부 조인
  • 컬렉션은 경로 탐색의 , 탐색은 명시적 조인으로 별칭을 부여해 가능
  • 경로 탐색 위치는 어디는 생성되는 SQL from 조인

 

묵시적 조인은 일어나는 상황을 한눈에 파악하기 어렵다. 따라서 가급적 명시적 조인을 사용한다.

서브 쿼리

SQL 다르게 WHERE, HAVING 절에만 사용가능하다.

 

 

서브 퀴리 함수

  • [NOT] EXISTS (서브쿼리)
    서브 쿼리 결과가 존재하면
  • {ALL | ANY | SOME} (서브쿼리)
    ALL
    모두 만족
    ANY, SOME 하나라도 만족
    비교
    연산자와 함께 사용한다.
  • [NOT] IN (서브쿼리)
    서브쿼리 결과 하나라도 같은 것이 존재하면

 

 

 

 

 

 

jpql some sql에서 any

 

 

 

조건식

타입 표현

대소문자 구분은 없다.

문자 작은 따옴표
작은 따옴표 표현은 작은 따옴표 연속
'문자'
'She''s'
숫자 L, D, F 10L, 10.0D, 10.0F
날짜 DATE{d 'yyyy-mm-dd'}
TIME{t 'hh:mm:ss'}
DATETIME{ts 'yyyy-mm-dd hh:mm:ss.f'}
{d '2022-12-22'}
{t '23:59:59'}
{ts '2023-12-23' 23:59:59.123'}
불린 TRUE, FALSE
열거형 패키지 포함 전체 경로 kr.co.test.EnumType.A
엔티티 타입 엔티티 타입을 표현, 주로 상속과 관련해서 사용 TYPE(m) = Member

 

연산자 우선 순위

  • 경로 탐색 연산
    .
  • 수학 연산
    +, -, *, /
  • 비교 연산
    =, >, >=, <, <=, <>,
    [NOT] BETWEEN,
    [NOT] LIKE,
    [NOT] IN,
    IS [NOT] NULL,
    IS [NOT] EMPTY,
    [NOT] MEMEBER [OF],
    [NOT] EXISTS
  • 논리 연산
    NOT, AND, OR

 

 

컬렉션

컬렉션 식은 컬렉션에만 사용하는 특별한 기능

 

컬렉션 비교

  • {컬렉션 연관 경로} IS [NOT] EMPTY
    컬렉션이 비었으면

 

 

 

컬렉션의 멤버

  • {엔티티나 } [NOT] MEMBER [OF] {컬렉션 연관 경로}
    컬렉션에 포함된 값이면

 

 

스칼라

https://openjpa.apache.org/builds/2.4.2/apache-openjpa/docs/jpa_langref.html#jpa_langref_scalar_expressions

 

https://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch11.html#ql-exp-functions

 

문자함수

  • CONCAT(문자, 문자,)
    HQL
      || 지원한다
  • SUBSTRING(문자, 위치, 길이)
  • TRIM(문자)

TRIM( BOTH 'A' FROM 'AAAA 문자 AAAA')

  • LOWER(문자)
  • UPPER(문자)
  • LENGTH(문자)
  • LOCATE(찾을값, 대상값 [, 검색 시작 위치])

 

수학 함수

  • ABS(숫자)
  • SQRT(숫자)
  • MOD(숫자, 제수)
  • SIZE(컬렉션)
    컬렉션 크기
  • INDEX(별칭)
    리스트 타입 컬렉션 위치 값을 구한다. @OrderColumn 사용하는 경우만 사용 가능

 

날짜 함수

  • CURRENT_DATE
  • CURRENT_TIME
  • CURRENT_TIMESTAMP

HQL 주어진 날짜에서 특정 값을 추출하는 함수를 지원한다.

 

CASE

단순 방법

  • CASE
    WHEN {
    조건식} THEN {결과}
    ELSE {
    결과}
    END

 

기본 방법

  • CASE
    WHEN {
    조건식} THEN {결과}
    WHEN {
    조건식} THEN {결과}
    ELSE {
    결과}
    END

 

 

NULLIF

  • NULLIF ({}, {비교값})
    값이 같으면 NULL 반환
    아니면
    반환

 

COALESCE

  • COALESCE({1}, {값2}, …)
    앞에서 부터 NULL 아니면, 값을 반환

 

다형성 쿼리

JPQL 부모 엔티티를 조회하면 자식 엔티티도 함께 조회

싱글 테이블 전략



@Entity
@Data
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        
        private String name;
        private int price;
        private int stockQuantity;
}


@Entity
@DiscriminatorValue("B")
@Data
public class Book extends Item{
        private String author;
        private String isbn;
}


@Entity
@DiscriminatorValue("M")
@Data
public class Movie extends Item{
        private String director;
        private String actor;
}


@Entity
@DiscriminatorValue("A")
@Data
public class Album extends Item{
        private String artist;
        private String etc;
}

 

 

 

조인 테이블 전략

 

테이블 클래스 전략

 

 

TYPE

엔티티 상속 구조에서 조회 대상을 특정 자식 타입으로 특정할 주로 사용한다.

 

조인 테이블 전략

 

테이블 클래스 전략

 

 

단일 테이블 전략

 

 

TREAT

상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 사용

FROM, WHERE 절에만 사용
하이버네이트는 SELECT 절도 사용 가능

 

 

사용자 정의 함수 호출

따로 자기가 사용하는 DB 방언을 상속해 직접 구현하고 등록해야 한다.

 

기타 정리

  • enum 동등 비교 연산만 된다
  • 인베디드 타입은 비교를 지원하지 않는다

 

EMPTY STRING

JPA 표준은 '' 길이 0 EMPTY STRING으로 취급한다.

DB 마다 ''   NULL 처리하는 경우가 있어 조심해야 한다.

 

NULL 정의

  • 없는 , NULL 수학적 연산을 시도하면 전부 NULL
  • NULL == NULL FALSE 이다. 예산이 불가능
     

  • NULL 연산은 전용 연산자를 사용한다

 

 

 

엔티티 직접 사용

 

 

 

JPQL 에서 엔티티 객체를 직접 사용하면, sql에서 기본키를 사용한다.

 

 

 

 

엔티티를 직접 사용하는 경우 파라미터도 기본 키로 치환된다.

다만, 엔티티를 직접 사용하는 경우 파라미터 인자로 엔티티를 받아야 한다.

"기본키 = 엔티티 파라미터" 또는 "엔티티 파라미터 = 기본키" 이렇게 불일치하는 경우는 안된다.

 

엔티티 직접 사용은 기본 뿐만 아니라 외래 키에도 동일하다.

 

 

 

Named 쿼리 : 정적 쿼리

JPQL 크게 동적 쿼리 / 정적 쿼리로 나눌 있다.

 

  • 동적 쿼리
    JPQL 직접 문자열로 완성해서 넘기는 쿼리
    JPQL 작성을 동적으로 가능
  • 정적 쿼리
    미리
    정의한 쿼리에 이름을 부여해 필요할 사용
    정의하면 변경 없음(정적)

 

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱한다.

따라서 로딩 시점에 오류 파악이 가능하다. 추가로 파싱된 쿼리를 재사용한다.

 

@NamedQuery 또는 xml 정의한다.

 

@NamedQuery

 

 

 

XML 정의

어노테이션으로 작성하는게 편리하나, 자바는 특성상 멀티라인 편집을 지원하지 안아 불편한 점이 존재한다.

이럴 사용한다.

 

우선권

XML 애노티에션에 같은 설정이 존재하면 XML 우선권을 가진다.

JPA뿐만 아니라 거의 모든 설정이 규칙을 따른다.

 

Criteria

JPQL 자바 코드로 작성하도록 도와주는 빌더 클래스 API

 

코드로 SQL 작성하기 때문에 컴파일 타임에 오류를 발견할 있고, 동적 쿼리 작성이 쉬워진다.

 

다만, Criteria 자체가 가독성이 낮고, 복잡하다

 

기초

 

 

조회

엔티티 반환, 컬럼 조회

 

 

 

 

DISTINCT

 

 

NEW, construct()

 

 

 

 

튜플

Map 비슷한 특별한 반환 객체

별칭은 필수

 

 

 

Object[] 컬럼들을 다루는 것보다 훨씬 안전하다.

 

집합

 

 

조인

 

 

JoinType 생략하면 기본이 내부 조인

 

 

 

서브 쿼리

일반 서브 쿼리

 

 

상호 관련 서브 쿼리

 

 

IN

 

 

CASE

 

 

파라미터 정의

 

 

앞서 예제랑 사실 차이가 없다

? 부분이 바인딩 변수가 들어갈 자리인데

하이버네이트는 그냥 where 값을 넣어도 PrepareStatement 처리한다.

 

 

네이티브 함수 호출

 

 

동적 쿼리

 

 

코드로 동적 쿼리를 작성할 있어, 문자열로 동적 쿼리를 작성할 발생하는 문제가 없다.

 

Criteria 메타 모델 API

많은 부분을 코드로 대체했지만, 값을 가져오는 부분은 문자열을 직접 다룬다.

오타가 있고, 부분은 컴파일 시점에오류로 감지할 없다.

 

메타 모델 적용 방법

https://docs.jboss.org/hibernate/stable/jpamodelgen/reference/en-US/html_single/#d0e76

 

pom.xml

의존성 추가

컴파일 설정

엔티티 이름 + _ 클래스가 생성된 메타 모델이다.

정식 명칭은 Canonical Metamodel

 

이제 절대 오타가 없다.

 

 

QueryDSL

JPQL 쿼리 빌더 클래스

Criteria 보다 직관적이고 이해가 쉽다.

조회에 특화되어 있다.

 

QueryDSL 설정

java 상표권 문제로 jakarta 바뀌면서 자동 생성 실패하는 사람이 설정을 하면 가능성이 높다.

 

 

Q 시작하는 클래스가 Q클래스

 

시작

 

Criteria 비교해서 훨씬 이해하기 쉽다.

 

검색 조건 쿼리

참고용

ComparableExpressionBase

 

스테틱 임포트를 사용하면 간결한 표현력을 얻을 있다

모르는 사람이 봐도 SQL 지식만 있다면 바로 이해가 정도로 표현력이 직관적이다.

 

페이징

 

 

그룹

 

 

조인

내부 조인

 

 

외부 조인

 

 

세타 조인

 

 

서브 쿼리

 

 

DTO 반환

 

생성자는 인수 리스트 타입을 맞추면 된다.

나머지 방법은 필드명이 일치해야 한다. 불일치 경우 별칭을 주어 Dto 필드에 맞추면 된다.

 

 

 

수정, 삭제 배치 쿼리

JPQL 배치 쿼리와 같이 영속성 컨텍스트는 무시하고 DB 직접 쿼리한다는 점을 유념해야 한다.

 

 

 

동적 쿼리

 

 

메소드 위임

기능을 사용하면 쿼리 타입 검색 조건을 Q클래스로 만들 있다.

static 메서드로 만들고 @QueryDelegate 어노테이션을 사용하면

위와 같이 Q클래스에 해당 메서드를 위임 구성으로 호출하는 메서드가 생성된다.

 

 

 

 

 

네이티브 SQL

JPQL 표준 SQL 지원하는 대부분을 커버할 있다. 하지만 특정 DB 종속적인 기능을 지원하진 못한다.

때로는 DB 종속적인 기능이 필요할 때가 있다.

 

  • 특정 DB 지원하는 함수
  • 특정 DB 지원하는 힌트
  • UNION, INTERSECT 같은 집합 연산자
  • 특정 DB 지원하는 문법

 

JPA SQL 직접 사용할 있는 기능을 지원하는데. 이럴 경우 JDBC 직접 다루는 것과 차이는 네이티브 SQL 사용하면 엔티티를 조회할 있고 JPA 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 있다 점이 다르다.

 

네이티브 SQL 사용

 

엔티티 조회

 

 

조회

스칼라 값으로 조회했으므로, 엔티티가 아니라서 영속성 컨텍스트로 관리되지 않는다.

 

 

결과 매핑 사용

엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡할 경우 사용한다.

@SqlResultSetMapping 애노테이션으로 결과 매핑을 정의한다.

 

 

 

 

 

결과 매핑 애노테이션

@SqlResultSetMapping

@EntityResult

@FieldResult

@ColumnResult

 

Named 네이티브 SQL

 

 

 

 

Named 쿼리는 XML 정의할 있다.

XML 정의하는 만큼 자바 코드에서 가장 불편한 멀티라인 문자열 처리에서 자유롭다.

, 라인 수가 많고 SQL 최적화해야 하는 경우엔 어노테이션보다 XML 정의하는게 편하다.

 

어느 쪽으로 정의하던 이를 사용하는 코드는 변경이 없다.

 

페이징

 

 

스토어드 프로시저

 

순서 기반 파라미터 호출

 

 

 

이름 기반 파라미터 호출

 

 

Named 스토어드 프로시저

역시 애노테이션 방식 대신 XML 방식으로 기술할 있다.

 

 

 

 

객체지향 쿼리 심화

  • 벌크 연산(대용량 데이터 처리)
  • JPQL 영속성 컨텍스트
  • JPQL 플러시 모드

 

벌크 연산

대용량 엔티티를 모두 수정, 삭제를 변경 감지 기능으로 처리하는 것은 시간이 너무 오래 걸린다.

이럴 벌크 연산을 사용한다.

 

전체 가격을 10% 인상

 

벌크 연산 주의점

 

 

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.

따라서 영속성 컨텍스트는 캐시된 엔티티를 반환했다.

 

 

해결 방안

 

 

 

 

 

벌크 연산은 영속성 컨텍스트와 2 캐시는 무시하고, DB 직접 질의하기 때문에 영속성 컨텍스트와 DB 사이 데이터 불일치가 발생할 있다.

가능하면 벌크 연산을 가장 먼저 수행하는 것이 좋다. 불가능하다면, 벌크 연산 이후 바로 영속성 컨텍스트를 초기화해야 한다.

 

영속성 컨텍스트와 JPQL

쿼리 영속 상태인 것과 아닌

JPQL 엔티티, 임베디드 타입, 타입 등을 조회할 있다.

하지만, 엔티티만이 영속성 컨텍스트에서 관리된다.

 

JPQL 조회한 엔티티와 영속성 컨텍스트

 

p1 조회한 쿼리

p2 영속성 컨텍스트에 이미 있어 조회를 하지 않음

 

p3 조회 쿼리

JPQL 일단 DB 먼저 질의를 한다. 그리고 조회된 결과 엔티티의 식별자를 영속성 컨텍스트에 있은 엔티티 식별자와 비교 이미 존재하면 조회된 엔티티를 버린다. 존재 하지 않으면, 영속성 컨텍스트에 저장한다.

 

기존 엔티티를 버리고, 새로운 엔티티르 대체하지 않는 이유

영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장한다.

JPA 조회하던, JPQL 조회하던, 같은 인스턴스임을 보장해야 한다.

추가로 영속화된 엔티티가 수정 중이라면, 새로 대체할 경우 수정 중인 데이터가 유실된다.

 

find(), JPQL

find() 메서드는 엔티티를 영속성 컨텍스트에서 찾고 없으면 DB에서 찾는다. 이런 동작 방식 때문에 영속성 컨텍스트를 1 캐시라 부른다.

 

JPQL 항상 데이터베이스에 SQL 실행해서 결과를 조회한다.

 

JPQL 플러시 모드

플러시는 영속성 컨텍스트의 변경 내역을 DB 동기화하는

 

 

 

AUTO 트랜잭션 커밋 직전 또는 쿼리 실행 직전에 자동으로 플러시를 호출한다.

COMMIT 커밋 시에만 플러시를 호출한다.

 

쿼리와 플러시 모드

JPQL 영속성 컨텍스트를 건너뛰고 DB 바로 조회한다.

, JPQL 실행 전에 영속성 컨텍스트를 플러시하는 작업이 필요하다.

 

 

 

하지만, 해당 식별자로 이미 값이 존재해 버려진다.

 

 

 

커밋 직전에 더티 채킹으로 영속성 컨텍스트의 변경 사항이 DB 동기화된다.

 

 

 

 

플러시 모드와 최적화

FlushModeType.COMMIT 모드는 트랜잭션 커밋 시에만 플러시 한다.

잘못하면 영속성 컨텍스트와 DB 사이 데이터 무결성이 깨질 수가 있다.

 

FlushModeType.COMMIT 모드는 플러시 횟수를 줄여 최적화할 제한적으로 사용되야 한다.

 

만약 JPA 사용해 DB 질의하는 것이 아닌 직접 JDBC 사용하는 경우, JPA 이를 감지할 방법이 없기 때문에 무조건 수동으로 플러시를 해줘야 한다.

 

정리

  • JPQL SQL 추상화했다. 특정 DB SQL 의존하지 않는다.
  • Criteria, QueryDSL JPQL 생성 빌더 역할만 한다.
  • Criteria JPA 공식 기능, QueryDSL 오픈 소스지만 직관적이고 쉬워서 자주 사용된다.
  • JPA로도 네이티브 SQL 실행할 있다.
  • JPQL 벌크 연산을 지원한다

'개발 > JPA' 카테고리의 다른 글

13 웹 애플리케이션과 영속성 관리  (1) 2024.06.10
12 스프링 데이터 JPA  (1) 2024.06.03
09 값 타입  (0) 2024.05.20
08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

프록시

엔티티를 조회할 반드시 연관 엔티티들이 사용되진 않는다.

 

JPA 사용하는 시점에 조회한다. 사용하는 시점에 조회흘 수행하려면, 객체의 메소드에 대한 흐름을 제어해야 한다. JPA 내부적으로 프록시를 이용해 지연 로딩을 수행한다.

 

 

프록시 특징

  • 프록시 객체는 처음 사용할 번만 초기화
  • 프록시는 원본 엔티티를 상속한 객체로 타입 체크 주의
  • 영속성 컨텍스트에 이미 찾는 엔티티가 있으면, em.getReference() 호출해도 원본 객체를 반환
  • 초기화는 영속성 컨텍스트의 도움을 받는다. 따라서 준영속 상태가 되서 사용을 하면 예외가 발생한다.

 



@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        
        @ManyToOne
        private Team team;
        private String name;
}


@Entity
@Data
public class Team {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String name;
}

 

        private static void find() {
                logic(em -> {
                        Member member = em.find(Member.class, 1L);
                        System.out.println("#########################################");
                        System.out.println(member.getClass().getSuperclass());
                        System.out.println(member.getClass());
                        
                        // Member 프록시를 반환
                        Member reference = em.getReference(Member.class, 1L);
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(reference.getClass().getSuperclass());
                        System.out.println(reference.getClass());
                });
        }

 

 



        private static void find2() {
                logic(em -> {
                        // 아무것도 수행 안함
                        Member reference = em.getReference(Member.class, 1L);
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(reference.getClass().getSuperclass());
                        System.out.println(reference.getClass());
                        
                        Member member = em.find(Member.class, 1L);
                        System.out.println("#########################################");
                        System.out.println(member.getClass().getSuperclass());
                        System.out.println(member.getClass());
                });
        }

 

 



        private static void find3() {
                logic(em -> {
                        Member reference = em.getReference(Member.class, 1L);
                        //준영속
                        em.detach(reference);
                        
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(reference.getClass().getSuperclass());
                        System.out.println(reference.getClass());
                });
        }

 

 

프록시와 식별자



        private static void find4() {
                logic(em -> {
                        Member reference = em.getReference(Member.class, 1L);
                        System.out.println("#########################################");
                        System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference));
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference));
                });
        }

 

 

 

즉시 로딩과 지연 로딩

  • 즉시 로딩
    연관된
    엔티티도 함께 조회
  • 지연 로딩
    연된된
    엔티티 사용할 조회

프록시는 연관된 엔티티를 지연 로딩할 쓰인다.

 

즉시 로딩

 

즉시 로딩은 연관된 엔티티를 함께 보기 위해 조인 쿼리를 사용한다.

 

null 제약조건과 JPA 조인 전략

sql 보면 외부 조인이다. 이유는 Member 테이블에 team_id null 허용하기 때문이다.

 

 

 

 

@JoinColumn nullable 속성에 외래 키는 null 허용하지 않는다는 제약을 주면, 내부 조인 사용해 외부 조인보다 좋은 성능을 기대할 있다.

 

동일한 결과

 

JPA 선택적 관계면 외부조인을 필수적 관계면 내부 조인을 사용한다.

 

지연 로딩

 

 

 

 

즉시 로딩, 지연 로딩 정리

지연로딩

연관된 엔티티를 프록시로 조회

연관된 엔티티는 실제 사용 DB 질의한다

 

즉시로딩

연관된 엔티티도 조인을 통해 번에 조회

 

지연 로딩 활용

JPA 기본 페치 전략

연관된 엔티티가 하나면 즉시로딩

컬렉션이면 지연로딩

 

 

추천하는 방법은 모든 연관관계에 지연 로딩을 사용하고, 필요한 곳만 즉시 로딩을 사용해 최적화한다.

 

SQL 개발하면, 시도조차 못하는 것이 지연로딩 이를 적극적으로 이용하자

 

컬렉션에 FetchType.EAGER 사용 주의점

컬렉션 하나 이상 즉시 로딩을 피한다.

엔티티 컬렉션은 DB 보면 일대다 조인을 의미한다.

일대다 조인은 데이터가 로우 만큼 증가한다.

컬렉션 있다면, C1 * C2 만큼 데이터를 반환할 있다.

JPA 이렇게 조호된 결과를 메모리에서 필터링한다.

절대 이상 컬렉션을 즉시 로딩하지 않는다.

 

컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

다대일의 경우 null 제약 조건에 따라 null 허용이면, 외부조인

null 불가이면, 항상 외래 키가 있다는 뜻이고, 외래 키는 참조 테이블에 키가 있다는 것을 의미한다. 따라서 내부조인을 한다.

 

일대다의 경우 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

 

 

영속성 전이: CASCADE

 



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        
        @OneToMany(mappedBy = "parent")
        private List<Child> childs = new ArrayList<>();
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
}


Parent p1 = new Parent();
em.persist(p1);


Child c1 = new Child();
c1.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용
em.persist(c1);


Child c2 = new Child();
c2.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c2); // 부모 -> 자식 그래프 탐색용
em.persist(c2);

 

JPA에서 엔티티를 저장할 연관된 모든 엔티티는 영속 상태여야 한다.

 

영속성 전이: 저장



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        
//        @OneToMany(mappedBy = "parent")
        @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
        private List<Child> childs = new ArrayList<>();
}

부모를 영속화할 연관된 자식들도 영속화한다.



Parent p1 = new Parent();
em.persist(p1);


Child c1 = new Child();
c1.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용


Child c2 = new Child();
c2.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c2); // 부모 -> 자식 그래프 탐색용

 

 

영속성 전이가 되어 Child 영속화된 것을 있다.

 

영속성 전이 자체는 연관관계 매핑과 아무런 관계가 없다. 단순히 영속화 편리성을 제공한다.

 

영속성 전이: 삭제

 

Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa`.`Child`, CONSTRAINT `FKlh67j1n7x7gt59u0pbkwqh6o6` FOREIGN KEY (`parent_id`) REFERENCES `Parent` (`parent_id`))

그냥 삭제 시도 외래 제약 조건 때문에 삭제를 못한다.

 

외래 제약 조건을 따져 순서에 맞게 제거가 된다.

 

영속성 전이는 플러시 시점에 전이가 발생한다.

 

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 삭제하는 기능을 고아 객체 제거라 한다.

부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.

 



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        
        @OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = CascadeType.PERSIST)
        private List<Child> childs = new ArrayList<>();
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
}
// 저장
Parent p1 = new Parent();
em.persist(p1);


for (int i = 0; i < 10; i++) {
        Child c1 = new Child();
        c1.setParent(p1);       // 자식 -> 부모 연관관계
        p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용
        em.persist(c1);
}


Parent p1 = em.find(Parent.class, 1L);
p1.getChilds().clear();

 

 

 

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

부모 엔티티를 통해 자식 엔티티 생명 주기를 관리할 있게 된다.



public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                logic(save());
                logic(remove());
                emf.close();
        }


        private static Consumer<EntityManager> remove() {
                return em -> {                
                        Parent p1 = em.find(Parent.class, 1L);
                        p1.getChilds().clear();
                };
        }


        private static Consumer<EntityManager> save() {
                return em -> {
                        
                        Parent p1 = new Parent();
                        
                        for (int i = 0; i < 10; i++) {
                                Child c1 = new Child();
                                c1.setParent(p1);     
                                p1.getChilds().add(c1);
                        }
                        em.persist(p1);
                };
        }


        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                try {
                        em.getTransaction().begin();
                        logic.accept(em);
                        em.getTransaction().commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        em.getTransaction().rollback();
                } finally {
                        em.close();
                }
        }
}

 

 

실전 예제

연관관계 관리

 

나머지 예제 그대로 바뀐 파일 코드만



@Entity
@Table(name = "orders")
@Data
public class Order extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
        
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
        
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
        private List<OrderItem> orderItems = new ArrayList<>();
        
        @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
        @JoinColumn(name = "delivery_id")
        private Delivery delivery;
        
        private LocalDateTime orderDate;
        
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
        // 연관관계 편의 메소드
        public void setMember(Member member) {
                if (this.member != null) {
                        this.member.getOrders().remove(this);
                }
                this.member = member;
                member.getOrders().add(this);
        }
        public void addOrderItem(OrderItem orderItem) {
                orderItems.add(orderItem);
                orderItem.setOrder(this);
        }
        
        public void setDelivery(Delivery delivery) {
                this.delivery = delivery;
                delivery.setOrder(this);
        }
}


Delivery delivery = new Delivery();
OrderItem orderItem1 = new OrderItem();
OrderItem orderItem2 = new OrderItem();


Order order = new Order();
order.setDelivery(delivery);;
order.addOrderItem(orderItem1);
order.addOrderItem(orderItem2);


em.persist(order);

 

 

 

 

'개발 > JPA' 카테고리의 다른 글

12 스프링 데이터 JPA  (1) 2024.06.03
10 객체지향 쿼리 언어  (0) 2024.05.27
08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06
06 다양한 연관관계 매핑  (0) 2024.04.14

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

프록시

엔티티를 조회할 반드시 연관 엔티티들이 사용되진 않는다.

 

JPA 사용하는 시점에 조회한다. 사용하는 시점에 조회흘 수행하려면, 객체의 메소드에 대한 흐름을 제어해야 한다. JPA 내부적으로 프록시를 이용해 지연 로딩을 수행한다.

 

 

프록시 특징

  • 프록시 객체는 처음 사용할 번만 초기화
  • 프록시는 원본 엔티티를 상속한 객체로 타입 체크 주의
  • 영속성 컨텍스트에 이미 찾는 엔티티가 있으면, em.getReference() 호출해도 원본 객체를 반환
  • 초기화는 영속성 컨텍스트의 도움을 받는다. 따라서 준영속 상태가 되서 사용을 하면 예외가 발생한다.

 



@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        
        @ManyToOne
        private Team team;
        private String name;
}


@Entity
@Data
public class Team {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String name;
}

 

        private static void find() {
                logic(em -> {
                        Member member = em.find(Member.class, 1L);
                        System.out.println("#########################################");
                        System.out.println(member.getClass().getSuperclass());
                        System.out.println(member.getClass());
                        
                        // Member 프록시를 반환
                        Member reference = em.getReference(Member.class, 1L);
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(reference.getClass().getSuperclass());
                        System.out.println(reference.getClass());
                });
        }

 

 



        private static void find2() {
                logic(em -> {
                        // 아무것도 수행 안함
                        Member reference = em.getReference(Member.class, 1L);
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(reference.getClass().getSuperclass());
                        System.out.println(reference.getClass());
                        
                        Member member = em.find(Member.class, 1L);
                        System.out.println("#########################################");
                        System.out.println(member.getClass().getSuperclass());
                        System.out.println(member.getClass());
                });
        }

 

 



        private static void find3() {
                logic(em -> {
                        Member reference = em.getReference(Member.class, 1L);
                        //준영속
                        em.detach(reference);
                        
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(reference.getClass().getSuperclass());
                        System.out.println(reference.getClass());
                });
        }

 

 

프록시와 식별자



        private static void find4() {
                logic(em -> {
                        Member reference = em.getReference(Member.class, 1L);
                        System.out.println("#########################################");
                        System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference));
                        // 첫 사용 한 번만 초기화 수행
                        reference.getTeam();
                        System.out.println("#########################################");
                        System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference));
                });
        }

 

 

 

즉시 로딩과 지연 로딩

  • 즉시 로딩
    연관된
    엔티티도 함께 조회
  • 지연 로딩
    연된된
    엔티티 사용할 조회

프록시는 연관된 엔티티를 지연 로딩할 쓰인다.

 

즉시 로딩

 

즉시 로딩은 연관된 엔티티를 함께 보기 위해 조인 쿼리를 사용한다.

 

null 제약조건과 JPA 조인 전략

sql 보면 외부 조인이다. 이유는 Member 테이블에 team_id null 허용하기 때문이다.

 

 

 

 

@JoinColumn nullable 속성에 외래 키는 null 허용하지 않는다는 제약을 주면, 내부 조인 사용해 외부 조인보다 좋은 성능을 기대할 있다.

 

동일한 결과

 

JPA 선택적 관계면 외부조인을 필수적 관계면 내부 조인을 사용한다.

 

지연 로딩

 

 

 

 

즉시 로딩, 지연 로딩 정리

지연로딩

연관된 엔티티를 프록시로 조회

연관된 엔티티는 실제 사용 DB 질의한다

 

즉시로딩

연관된 엔티티도 조인을 통해 번에 조회

 

지연 로딩 활용

JPA 기본 페치 전략

연관된 엔티티가 하나면 즉시로딩

컬렉션이면 지연로딩

 

 

추천하는 방법은 모든 연관관계에 지연 로딩을 사용하고, 필요한 곳만 즉시 로딩을 사용해 최적화한다.

 

SQL 개발하면, 시도조차 못하는 것이 지연로딩 이를 적극적으로 이용하자

 

컬렉션에 FetchType.EAGER 사용 주의점

컬렉션 하나 이상 즉시 로딩을 피한다.

엔티티 컬렉션은 DB 보면 일대다 조인을 의미한다.

일대다 조인은 데이터가 로우 만큼 증가한다.

컬렉션 있다면, C1 * C2 만큼 데이터를 반환할 있다.

JPA 이렇게 조호된 결과를 메모리에서 필터링한다.

절대 이상 컬렉션을 즉시 로딩하지 않는다.

 

컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

다대일의 경우 null 제약 조건에 따라 null 허용이면, 외부조인

null 불가이면, 항상 외래 키가 있다는 뜻이고, 외래 키는 참조 테이블에 키가 있다는 것을 의미한다. 따라서 내부조인을 한다.

 

일대다의 경우 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

 

 

영속성 전이: CASCADE

 



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        
        @OneToMany(mappedBy = "parent")
        private List<Child> childs = new ArrayList<>();
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
}


Parent p1 = new Parent();
em.persist(p1);


Child c1 = new Child();
c1.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용
em.persist(c1);


Child c2 = new Child();
c2.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c2); // 부모 -> 자식 그래프 탐색용
em.persist(c2);

 

JPA에서 엔티티를 저장할 연관된 모든 엔티티는 영속 상태여야 한다.

 

영속성 전이: 저장



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        
//        @OneToMany(mappedBy = "parent")
        @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
        private List<Child> childs = new ArrayList<>();
}

부모를 영속화할 연관된 자식들도 영속화한다.



Parent p1 = new Parent();
em.persist(p1);


Child c1 = new Child();
c1.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용


Child c2 = new Child();
c2.setParent(p1);       // 자식 -> 부모 연관관계
p1.getChilds().add(c2); // 부모 -> 자식 그래프 탐색용

 

 

영속성 전이가 되어 Child 영속화된 것을 있다.

 

영속성 전이 자체는 연관관계 매핑과 아무런 관계가 없다. 단순히 영속화 편리성을 제공한다.

 

영속성 전이: 삭제

 

Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa`.`Child`, CONSTRAINT `FKlh67j1n7x7gt59u0pbkwqh6o6` FOREIGN KEY (`parent_id`) REFERENCES `Parent` (`parent_id`))

그냥 삭제 시도 외래 제약 조건 때문에 삭제를 못한다.

 

외래 제약 조건을 따져 순서에 맞게 제거가 된다.

 

영속성 전이는 플러시 시점에 전이가 발생한다.

 

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 삭제하는 기능을 고아 객체 제거라 한다.

부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.

 



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        
        @OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = CascadeType.PERSIST)
        private List<Child> childs = new ArrayList<>();
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
}
// 저장
Parent p1 = new Parent();
em.persist(p1);


for (int i = 0; i < 10; i++) {
        Child c1 = new Child();
        c1.setParent(p1);       // 자식 -> 부모 연관관계
        p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용
        em.persist(c1);
}


Parent p1 = em.find(Parent.class, 1L);
p1.getChilds().clear();

 

 

 

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

부모 엔티티를 통해 자식 엔티티 생명 주기를 관리할 있게 된다.



public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                logic(save());
                logic(remove());
                emf.close();
        }


        private static Consumer<EntityManager> remove() {
                return em -> {                
                        Parent p1 = em.find(Parent.class, 1L);
                        p1.getChilds().clear();
                };
        }


        private static Consumer<EntityManager> save() {
                return em -> {
                        
                        Parent p1 = new Parent();
                        
                        for (int i = 0; i < 10; i++) {
                                Child c1 = new Child();
                                c1.setParent(p1);     
                                p1.getChilds().add(c1);
                        }
                        em.persist(p1);
                };
        }


        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                try {
                        em.getTransaction().begin();
                        logic.accept(em);
                        em.getTransaction().commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        em.getTransaction().rollback();
                } finally {
                        em.close();
                }
        }
}

 

 

실전 예제

연관관계 관리

 

나머지 예제 그대로 바뀐 파일 코드만



@Entity
@Table(name = "orders")
@Data
public class Order extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
        
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
        
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
        private List<OrderItem> orderItems = new ArrayList<>();
        
        @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
        @JoinColumn(name = "delivery_id")
        private Delivery delivery;
        
        private LocalDateTime orderDate;
        
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
        // 연관관계 편의 메소드
        public void setMember(Member member) {
                if (this.member != null) {
                        this.member.getOrders().remove(this);
                }
                this.member = member;
                member.getOrders().add(this);
        }
        public void addOrderItem(OrderItem orderItem) {
                orderItems.add(orderItem);
                orderItem.setOrder(this);
        }
        
        public void setDelivery(Delivery delivery) {
                this.delivery = delivery;
                delivery.setOrder(this);
        }
}


Delivery delivery = new Delivery();
OrderItem orderItem1 = new OrderItem();
OrderItem orderItem2 = new OrderItem();


Order order = new Order();
order.setDelivery(delivery);;
order.addOrderItem(orderItem1);
order.addOrderItem(orderItem2);


em.persist(order);

 

 

 

 

'개발 > JPA' 카테고리의 다른 글

10 객체지향 쿼리 언어  (0) 2024.05.27
09 값 타입  (0) 2024.05.20
07 고급 매핑  (0) 2024.05.06
06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

상속 관계 매핑

RDB 상속이라는 개념이 없다.

모델링 관점에서 슈퍼타입 서브타입 관계가 있다.

 

ORM에서 상속 관계 매핑은 객체의 상속 구조와 DB 슈퍼타입 서브타입 관계를 매핑하는 것을 의미한다.

 

가지 전략

  • 각각의 테이블로 변환
    부모
    테이블, 자식 테이블 외래 키로 조인
    JPA
    에서 조인 전략이라 한다.
  • 통합 테이블로 변환
    하나의
    테이블로 객체 상속 관계 모든 필드를 컬럼으로 만든다.
    JPA
    단일 테이블 전략
  • 서브타입 테이블로 변환
    부모
    타입 테이블은 안만들고 대신, 부모 타입 속성을 서브 타입 마다 가지고 있음
    구현
    클래스 마다 테이블 전략

 

 

 

 

조인 전략

부모 테이블, 자식 테이블을 만들고 사이에 외래 키로 조인한다.

자식 테이블은 부모의 개인 키를 받아 외래 + 기본 키로 사용하는 식별관계가 된다.

객체에선 타입이라는 개념이 존재해 부모 타입, 서브 타입을 구별가능하지만, 테이블엔 그런게 없다. 따라서 타입 구분 컬럼 필요하다.

 

 

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE") // 기본 값이 DTYPE이다.
@Data
public abstract class Item {
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        
        private String name;
        private int price;
}
@Entity
@DiscriminatorValue("A")
@Data
public class Album extends Item {


        private String artist;
}
@Entity
@DiscriminatorValue("M")
@Data
public class Movie extends Item {
        private String director;
        private String actor;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "book_id")
@Data
public class Book extends Item{
        private String author;
        private String isbn;
}

 

상속 매핑은 부모 클래스에 해야 한다. @Inheritance 어노테이션으로 설정한다.

전략은 가지

 

@DiscriminatorColumn 어노테이션은 구분 컬럼을 설정한다.

값으로 자식 타입 테이블을 식별한다.

@DiscriminatorValue 어노테이션으로 자식 엔티티가 어떤 구분 값을 가질 설정한다.

@PrimaryKeyJoinColumn 어노테이션으로 자식 테이블의 기본 컬럼 이름을 변경할 있다.

 

 

장점

  • 테이블 정규화
  • 외래 참조 무결성 제약 조건 활용
  • 저장공간 효율적

단점

  • 조회 조인 사용으로 성능 저하 가능성
  • 데이터 저장 INSERT 2

특징

JPA 표준 명세엔 구분 컬럼이 필수지만, 하이버네이트 포함 몇몇 구현체는 구분 컬럼 없이도 동작한다.

 

단점으로 소개된 결과는 테이블이 쪼개진(정규화된) 결과로 파생되는 증상들이다.

 

 



        private static void delete() {
                logic(em -> {
                        Book book = em.find(Book.class, 1L);
                        em.remove(book);
                });
        }


        private static void findAndUpdate() {
                logic(em -> {
                        Book book = em.find(Book.class, 1L);
                        book.setIsbn("2222222");
                        book.setAuthor("현진건");
                        book.setName("운수좋은날");
                });
        }


        private static void save() {
                logic(em -> {
                        Book book = new Book();
                        book.setAuthor("윤흥길");
                        book.setIsbn("111111");
                        book.setPrice(5000);
                        book.setName("아홉켤레의구두로남은사내");
                        
                        em.persist(book);
                });
        }

 

 

 

 

 

단일 테이블 전략



@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE") // 기본 값이 DTYPE이다.
@Data
public abstract class Item {
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        
        private String name;
        private int price;
}

 

 

 

장점

  • 조인이 없어 조회가 편리하고, 빠르다

단점

  • 자식 엔티티가 매핑한 컬럼은 무조건 null 허용해야 한다.
    논리적으로 자식 엔티티들로 행에 모두 데이터를 채울 없음
  • 단일 테이블이 커질 있다.

 

특징

몇몇 구현체는 구분 컬럼이 없어도 동작 가능하다고 헀지만, 전략은 구분 컬럼이 필수다. @DiscriminatorColumn 필수, 자식 엔티티에 @DiscriminatorValue 없으면, 기본 값으로 자식 엔티티 이름을 사용한다.

 

 

구현 클래스마다 테이블 전략

 

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Data
public abstract class Item {
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        
        private String name;
        private int price;
}


@Entity
@Data
public class Album extends Item {


        private String artist;
}
@Entity
@Data
public class Book extends Item{
        private String author;
        private String isbn;
}

@Entity
@Data
public class Movie extends Item {
        private String director;
        private String actor;
}

 

 

일반적으로 추천하지 않는 전략, 사용하지

 

장점

  • 서브 타입 구분이 명확
  • not null 제약 조건 가능

단점

  • 여러 자식 테이블을 함께 조회 느리다. UNION 으로 행을 붙여야 한다.
    자식 테이블 통합 쿼리가 어렵다.
    일반 조인을 사용하면 컬럼으로 붙인다.

특징

  • 구분 컬럼이 없다.

 

 

@MappedSuperclass

부모 클래스 속성을 사용하되, 부모 엔티티를 테이블에 매핑하고 싶지 않을 사용한다.

상속 관계를 표현하는 목적이 아닌 단순히 공통의 매핑 정보만 제공할 목적으로 사용한다.

 

 

@MappedSuperclass
@Data
public abstract class BaseEntity {
        @Id
        @GeneratedValue
        private Long id;
        private LocalDateTime reg_date; // 등록일자
        private String reg_id; //등록자 ID
        private LocalDateTime mod_date; // 수정일자
        private String mod_id; // 수정자 ID
}

@Entity
@Data
public class Board extends BaseEntity {
        private String name;
        private String contents;
}



@Entity
@AttributeOverrides({
        @AttributeOverride(name = "id", column = @Column(name = "member_id"))
})
@Data
public class Member extends BaseEntity{
        private String name;
}

 

 

@AttributeOverride 어노테이션을 통해 상속받은 매핑 정보를 재정의할 있다.

 

@MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 영속성 컨텍스트에서 사용할 없다.

보통 인스턴스를 생성하지 못하도록 추상 클래스로 선언한다.

 

@Entity 원래 @Entity 클래스만 상속 받을 있지만, @MappedSuperclass도 상속 받을 있다.

 

복합 키와 식별 관계 매핑

DB 테이블 사이 외래 관계

  • 식별 관계
    외래
    키를 자신의 기본 키로 사용
    이때
    복합 키로 사용되어도 식별 관계이다.
  • 비식별 관계
    외래
    키로만 사용
    null 허용 여부에 따라 추가 분류

 

보통 비식별 관계를 사용하고, 필요한 곳만 식별 관계를 사용한다.

 

복합키 : 비식별 관계 매핑

방법 가지

  • @IdClass
    DB
    가까운 방법
  • @EmbeddedId
    객체지향에 가까운 방법

 

 

@IdClass

 

 



@Entity
@IdClass(MasterId.class)
@Data
public class Master {
        @Id
        @GeneratedValue
        @Column(name = "master_id1")
        private Long id1;
        
        @Id
        @GeneratedValue
        @Column(name = "master_id2")
        private Long id2;
        
        private String name;
}


@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class MasterId implements Serializable{
        private Long id1;
        private Long id2;
}


@Entity
@Data
public class Detail {
        
        @Id
        @GeneratedValue
        @Column(name = "detail_id")
        private Long id;
        
        @ManyToOne
        @JoinColumns({
                @JoinColumn(name = "fk_master_id1", referencedColumnName = "master_id1"),
                @JoinColumn(name = "fk_master_id2", referencedColumnName = "master_id2")
        })
        private Master master;
}

@IdClass 식별자 클래스 조건

  • 속성명이 같아야 한다.
  • Serializable 구현
  • equals, hashCode 재정의
  • 기본 생성자 필수
  • public 클래스

 



@Entity
@IdClass(Master.MasterId.class)
@Data
public class Master {
        @Id
        @GeneratedValue
        @Column(name = "master_id1")
        private Long id1;
        
        @Id
        @GeneratedValue
        @Column(name = "master_id2")
        private Long id2;
        
        private String name;
        
        @EqualsAndHashCode
        @AllArgsConstructor
        @NoArgsConstructor
        public static class MasterId implements Serializable{
                private Long id1;
                private Long id2;
        }


}

참고로 내장 클래스로 사용해도 된다.

 



        private static void find() {
                logic(em -> {
                        Master.MasterId masterId = new Master.MasterId(1L, 2L);
                        em.find(Master.class, masterId);
                        em.find(Detail.class, 1L);
                });
        }




        private static void save() {
                logic(em -> {
                        Master master = new Master();
                        master.setName("마스터");
                        
                        em.persist(master);
                        
                        Detail detail = new Detail();
                        detail.setMaster(master);
                        
                        em.persist(detail);
                        
                });
        }

 

 

 

@EmbeddedId

객체지향적 방법



@Entity
@Data
public class Master {
        @EmbeddedId
        private Master.MasterId id;
        
        private String name;
        
        @EqualsAndHashCode
        @AllArgsConstructor
        @NoArgsConstructor
        @Embeddable
        public static class MasterId implements Serializable{
                @Column(name = "master_id1")
                private Long id1;
                @Column(name = "master_id2")
                private Long id2;
        }
}


@Entity
@Data
public class Detail {
        
        @Id
        @GeneratedValue
        @Column(name = "detail_id")
        private Long id;
        
        @ManyToOne
        @JoinColumns({
                @JoinColumn(name = "fk_master_id1", referencedColumnName = "master_id1"),
                @JoinColumn(name = "fk_master_id2", referencedColumnName = "master_id2")
        })
        private Master master;
}


        private static void find() {
                logic(em -> {
                        Master.MasterId masterId = new Master.MasterId(1L, 2L);
                        em.find(Master.class, masterId);
                        em.find(Detail.class, 1L);
                });
        }




        private static void save() {
                logic(em -> {
                        Master master = new Master();
                        master.setId(new Master.MasterId(1L, 2L));
                        master.setName("마스터");
                        
                        em.persist(master);
                        
                        Detail detail = new Detail();
                        detail.setMaster(master);
                        
                        em.persist(detail);
                        
                });
        }

@GeneratedValue 사용이 불가능해서 제거 직접 키를 할당했다.

 

DB 전혀 바뀌는 없다. 단지 애플리케이션에서 매핑을 어떻게 하냐만 다를

 

 

 

식별자 클래스에 @Embeddable 애노테이션을 붙인다.

식별자 클래스에 직접 기본 키를 매핑한다.

식별자 클래스를 직접 필드로 넣고 @EmbeddedId 애노테이션을 붙인다.

 

식별자 클래스 조건

  • @Embeddable 붙임
  • Serializable 구현
  • equals, hashCode 재정의
  • 기본 생성자 필수
  • public 클래스일

 

영속성 컨텍스트는 식별자를 기반으로 엔티티를 관리한다.

식별자를 비교할 equals, hashCode 사용하므로

식별자 클래스는 반드시 재정의해야 한다.

 

@IdClass vs @EmbeddedId

장단점이 존재하므로, 중요한 것은 하나를 선택하면 일관성 있게 사용해야 한다.

 

 

 

복합 식별 관계 매핑

 

@IdClass 식별 관계

 



@Entity
@Data
public class A {
        @Id
        @Column(name = "a_id")
        private String id;
        private String name;;
}
@Entity
@IdClass(B.BId.class)
@Data
public class B {
        
        @Id
        @ManyToOne
        @JoinColumn(name = "a_id") // 외래 키 식별
        private A a;
        
        @Id
        @Column(name = "b_id")
        private String id;
        private String name;;
        
        @NoArgsConstructor
        @EqualsAndHashCode
        @Data
        public static class BId implements Serializable {
                private String a;  //public A a;
                private String id; //private String id;
        }
}
@Entity
@IdClass(C.CId.class)
@Data
public class C {
        
        @Id
        @ManyToOne
        @JoinColumns({
                @JoinColumn(name = "a_id"),
                @JoinColumn(name = "b_id")
        })
        private B b;
        
        @Id
        @Column(name = "c_id")
        private String id;
        
        private String name;
        
        @NoArgsConstructor
        @EqualsAndHashCode
        @Data
        public static class CId implements Serializable{
                private B.BId b;  //private B b;
                private String id; // private String id;
        }
}

 

 



        public static void main(String[] args) {
                save();
                find();
                emf.close();
        }




        private static void find() {
                logic(em -> {
                        A a = em.find(A.class, "A_ID");
                        
                        BId bId = new B.BId();
                        bId.setA("A_ID");
                        bId.setId("B_ID");
                        
                        B b = em.find(B.class, bId);
                        
                        CId cId = new C.CId();
                        cId.setB(bId);
                        cId.setId("C_ID");
                        
                        C c = em.find(C.class, cId);
                });
        }




        private static void save() {
                logic(em -> {
                        A a = new A();
                        a.setId("A_ID");
                        a.setName("A");
                        em.persist(a);
                        
                        B b = new B();
                        b.setA(a);
                        b.setId("B_ID");
                        b.setName("B");
                        em.persist(b);
                        
                        C c = new C();
                        c.setB(b);
                        c.setId("C_ID");
                        c.setName("C");
                        em.persist(c);
                });
        }

 

 

 

 

@EmbeddedId 식별 관계

@MapsId 사용이 필요

 



@Entity
@Data
public class A {
        @Id
        @Column(name = "a_id")
        private String id;
        private String name;;
}


@Entity
@Data
public class B {
        @MapsId("AId") //private String AId;
        @ManyToOne
        @JoinColumn(name = "a_id")
        private A a;
        
        @EmbeddedId
        private B.BId id;
        private String name;;
        
        @NoArgsConstructor
        @EqualsAndHashCode
        @Data
        @Embeddable
        public static class BId implements Serializable {
                private String AId; //@MapsId("AId")
                @Column(name = "b_id")
                private String id;
        }
}


@Entity
@Data
public class C {
        
        @MapsId("BId") //private B.BId BId;
        @ManyToOne
        @JoinColumns({
                @JoinColumn(name = "a_id"),
                @JoinColumn(name = "b_id")
        })
        private B b;
        @EmbeddedId
        private C.CId id;
        private String name;
        
        @NoArgsConstructor
        @EqualsAndHashCode
        @Data
        @Embeddable
        public static class CId implements Serializable{
                private B.BId BId; //@MapsId("BId")
                @Column(name = "c_id")
                private String id;
        }


}


        public static void main(String[] args) {
                save();
                find();
                emf.close();
        }




        private static void find() {
                logic(em -> {
                        A a = em.find(A.class, "A_ID");
                        
                        BId bId = new B.BId();
                        bId.setAId("A_ID");
                        bId.setId("B_ID");
                        
                        B b = em.find(B.class, bId);
                        
                        CId cId = new C.CId();
                        cId.setBId(bId);
                        cId.setId("C_ID");
                        
                        C c = em.find(C.class, cId);
                });
        }




        private static void save() {
                logic(em -> {
                        A a = new A();
                        a.setId("A_ID");
                        a.setName("A");
                        em.persist(a);
                        
                        B b = new B();
                        b.setA(a);
                        BId bId = new B.BId();
                        bId.setAId("A_ID");
                        bId.setId("B_ID");
                        b.setId(bId);
                        b.setName("B");
                        em.persist(b);
                        
                        C c = new C();
                        c.setB(b);
                        CId cId = new C.CId();
                        cId.setBId(bId);
                        cId.setId("C_ID");
                        c.setId(cId);
                        c.setName("C");
                        em.persist(c);
                });
        }

 

@EmbeddedId @IdClass 다르게 @Id 대신 @MapsId 사용한다.

@MapsId 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는

 

 

@IdClass, @EmbeddedId 엔티티에서 매핑 방식이기에 DB 테이블 생성, 조회 쿼리는 똑같다.

 

비식별 관계 구현

 

 



@Entity
@Data
public class A {
        @Id
        @GeneratedValue
        @Column(name = "a_id")
        private Long id;
        private String name;;
}


@Entity
@Data
public class B {
        @Id
        @GeneratedValue
        @Column(name = "b_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "a_id")
        private A a;
        
        private String name;;
}


@Entity
@Data
public class C {
        @Id
        @GeneratedValue
        @Column(name = "c_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "b_id")
        private B b;
        private String name;
}

 

 

비식별 관계에서 복합 키를 만들 필요가 없어 식별 관계 사용 복합 키를 사용한 것과 비교해 쉽다.

 

일대일 식별 관계

일대일 관계는 자식 테이블이 부모 테이블 기본 키를 외래 + 기본 키로 사용한다.

따라서 부모 테이블 기본 키가 복합 키가 아니면, 자식 테이블도 복합 키로 구성하지 않아도 된다.



@Entity
@Data
public class Board {
        @Id
        @GeneratedValue
        @Column(name = "board_id")
        private Long id;
        
        private String title;
        
        @OneToOne(mappedBy = "board")
        private BoardDetail boardDetail;
}


@Entity
@Data
public class BoardDetail {
        @Id
        private Long id;
        
        @MapsId
        @OneToOne
        @JoinColumn(name = "board_id")
        private Board board;
        
        private String content;
}

 

 

식별 컬럼이 하나면 값을 생략해도 된다.

 

 

식별, 비식별 관계의 장단점

DB 관점에서 비식별 관계를 선호하는 이유

부모 테이블의 기본 키를 자식 테이블로 전파한다.

특성 때문에 부모-자식 관계가 반복될 경우 가장 자식 테이블의 기본 키는 상위 모든 부모 테이블의 기본 키를 가지게 된다.

기본 키가 많은 수록 인덱스 부하, 조인 복잡하다.

 

기본 전파 특성 때문에 복합 키를 기본 키로 많들어야 하는 경우가 많다

 

식별 관계에서 기본 키로 자연 키를 사용할 경우가 많다. 경우 시간이 지나 비즈니스 요구사항이 바뀌면, 자식 모두에게 영향을 미쳐 변경이 힘들어 진다.

, 테이블 구조가 유연하지 못하다.

 

객체 관계 매핑 관점에서 비식별 관계를 선호하는 이유

일대일 관계를 제외하고, 식별 관계는 복합 키를 기본 키로 사용해야 한다.

, 기본 설정이 복잡하다.

 

생성 또한, 비식별 관계는 대리 자동 생성을 주로 사용하는 , 식별 관계는 복합 키가 많아 불가능하다.

 

식별 관계 장점으로 자식 테이블이 기본 키가 거대해 특수한 상황에서 조인없이 기본 인덱스로만 조회할 있다.

 

가급적 필수적 비식별 관계를 사용하고 기본 키는 Long 대리 키를 사용하자

필수적 비식별 관계여야 외부 조인을 사용하지 않는다.

 

 

조인 테이블

DB 테이블 연관관계 설계 방법 가지

  • 조인 컬럼 (외래 사용)
  • 조인 테이블 (테이블 사용)
    연관관계를 별도 테이블로 관리
    조인
    연관관계 테이블까지 조인해야 한다.
    다대다 관계를 일대다 - 다대일 관계로 풀기 위한 용도로 주로 사용

 

 

일대일 조인 테이블

 

외래 컬럼 모두에 유니크 제약조건이 필요하다

 



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
        
        @OneToOne
        @JoinTable(name = "parent_child",
                joinColumns = @JoinColumn(name = "parent_id"),
                inverseJoinColumns = @JoinColumn(name = "child_id", unique = true)
        )
        private Child child;
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
        @OneToOne(mappedBy = "child")
        private Parent parent;
}

 

 

 

 

일대다 조인 테이블

쪽에 속한 CHILD_ID 대한 유니크 제약 조건이 필요하다.

 

 

@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
        
        @OneToMany
        @JoinTable(name = "parent_child",
                joinColumns = @JoinColumn(name = "parent_id"),
                inverseJoinColumns = @JoinColumn(name = "child_id")
        )
        private List<Child> childs = new ArrayList<>();
}
@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
}

 

다대일 조인 테이블



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
        
        @OneToMany(mappedBy = "parent")
        private List<Child> childs = new ArrayList<>();
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
        
        @ManyToOne(optional = false)
        @JoinTable(name = "parent_child",
                joinColumns = @JoinColumn(name = "child_id"),
                inverseJoinColumns = @JoinColumn(name = "parent_id")
        )
        private Parent parent;
}

 

 

대다대 조인 테이블

 



@Entity
@Data
public class Parent {
        @Id
        @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
        
        @ManyToMany
        @JoinTable(name = "parent_child",
                uniqueConstraints = {@UniqueConstraint(columnNames = {"parent_id", "child_id"})},
                joinColumns = @JoinColumn(name = "parent_id"),
                inverseJoinColumns = @JoinColumn(name = "child_id")
        )
        private List<Child> childs = new ArrayList<>();
}


@Entity
@Data
public class Child {
        @Id
        @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
        
        @ManyToMany(mappedBy = "childs")
        private List<Parent> parent = new ArrayList<>();;
}

 

모든 @JoinTable 외래 키를 제외한 추가 컬럼이 필요 사용할 없다.

 

엔티티 하나에 여러 테이블 매핑

@SecondaryTable

엔티티에 여러 테이블을 매핑할 있다.



@Data
@Entity
@Table(name = "Board")
@SecondaryTable(name = "Board_Detail",
        pkJoinColumns = @PrimaryKeyJoinColumn(name = "board_detail_id"))
public class Board {
        @Id
        @GeneratedValue
        @Column(name = "board_id")
        private Long id;
        private String title;
        
        @Column(table = "Board_Detail")
        private String content;
}

 

 

 

@SecondaryTable 사용하지 않는 것이 좋다.

항상 테이블에서 조회하게 되며 최적화하기 힘들다.

각각 테이블에 매핑되었다면, 쪽만 조회 필요 조인으로 조회할 있다.

 

 

 

 

실전 예제

상속 관계 매핑 추가

  • @Inheritance, @DiscriminatorColumn, @DiscriminatorValue
  • @MappedSuperclass

 



@MappedSuperclass
@Data
public class BaseEntity {
        private LocalDateTime createdDate;
        private LocalDateTime lastModifiedDate;
}


@Entity
@Data
public class Member extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        
        private String name;
        private String city;
        private String strret;
        private String zipcode;
        
        @OneToMany(mappedBy = "member")
        @ToString.Exclude
        private List<Order> orders = new ArrayList<>();
}


@Entity
@Data
public class Delivery extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "delivery_id")
        private Long id;
        
        @OneToOne(mappedBy = "delivery")
        private Order order;
        private String city;
        private String street;
        private String zipcode;
        @Enumerated(EnumType.STRING)
        private DeliveryStatus status;
        
}


@Entity
@Table(name = "orders")
@Data
public class Order extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        
        @OneToMany(mappedBy = "order")
        private List<OrderItem> orderItems = new ArrayList<>();
        
        @OneToOne
        @JoinColumn(name = "delivery_id")
        private Delivery delivery;
        
        private LocalDateTime orderDate;
        
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
        // 연관관계 편의 메소드
        public void setMember(Member member) {
                if (this.member != null) {
                        this.member.getOrders().remove(this);
                }
                this.member = member;
                member.getOrders().add(this);
        }
        public void addOrderItem(OrderItem orderItem) {
                orderItems.add(orderItem);
                orderItem.setOrder(this);
        }
        
        public void setDelivery(Delivery delivery) {
                this.delivery = delivery;
                delivery.setOrder(this);
        }
}


@Entity
@Table(name = "order_item")
@Data
public class OrderItem extends BaseEntity {
        
        @Id
        @GeneratedValue
        @Column(name = "order_item_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "item_id")
        private Item item;


        @ManyToOne
        @JoinColumn(name = "order_id")
        private Order order;
        
        private int orderPrice;
        private int count;
        
        public void setOrder(Order order) {
                if (this.order != null) {
                        this.order.getOrderItems().remove(this);
                }
                this.order = order;
                order.getOrderItems().add(this);
        }
}


@Entity
@Data
public class Category extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "category_id")
        private Long id;
        
        private String name;
        
        @ManyToMany
        @JoinTable(name = "category_item",
                        joinColumns = @JoinColumn(name = "category_id"),
                        inverseJoinColumns = @JoinColumn(name = "item_id"))
        private List<Item> items = new ArrayList<>();
        
        // 카테고리 계층 구조를 위한 필드들
        @ManyToOne
        @JoinColumn(name = "parent")
        private Category parent;
        
        @OneToMany(mappedBy = "parent")
        private List<Category> child = new ArrayList<>();
        
        // 연관관계 메소드
        public void addChildCategory(Category child) {
                this.child.add(child);
                child.setParent(this);
        }
        public void addItem(Item item) {
                items.add(item);
        }
}


@Data
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        
        private String name;
        private int price;
        private int stockQuantity;
        
        @ManyToMany(mappedBy = "items")
        private List<Category> categories = new ArrayList<>();
}


@Entity
@DiscriminatorValue("A")
@Data
public class Album extends Item{
        private String artist;
        private String etc;
}


@Entity
@DiscriminatorValue("M")
@Data
public class Movie extends Item{
        private String director;
        private String actor;
}


@Entity
@DiscriminatorValue("B")
@Data
public class Book extends Item{
        private String author;
        private String isbn;
}

'개발 > JPA' 카테고리의 다른 글

09 값 타입  (0) 2024.05.20
08 프록시와 연관관계 관리  (1) 2024.05.13
06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22
04 엔티티 매핑  (1) 2024.02.26

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

엔티티 연관관계 매핑 고려사항

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

 

다중성

  • 다대일
  • 일대다
  • 일대일
  • 다대다

 

단방향, 양방향

테이블은 원래 양방향 이기 때문에 앤티티 방향을 의미

객체의 방향이란 객체 그래프 탐색이 가능한지 여부

 

연관관계의 주인

테이블은 하나의 외래 키로 테이블이 연관관계를 맺음

관리 포인트는 외래키 하나

엔티티는 방향 개로 양방향 연관관계를 맺음

관리 포인트 방향

 

차이를 보정하기 위해 하나의 엔티티에서 외래 키를 관리하도록 연관관계의 주인을 정함

주인이 아닌 반대편은 외래 키에 대해서 읽기 전용으로 동작

 

연관관계 주인이 아닌 곳은 mappedBy 속성으로 연관관계 주인 필드 이름을 명시한다.

 

다중성, 단방향, 양방향 고려한 경우의

  • 다대일
  • 일대다
  • 일대일
  • 다대다

 

다대일

DB 테이블의 다대일 관계는 항상 쪽에 외래 키가 있다.

따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

 

다대일 단방향

 

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToOne
        @JoinColumn(name = "team_id")
        private Team team;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {
        @Id
        @GeneratedValue
        @Column(name = "team_id")
        private Long id;
        private String name;
}

Member 에서 Team 탐색이 가능

 

다대일 양방향

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private String id;
        private String username;
        
        @ManyToOne
        @JoinColumn(name = "team_id")
        private Team team;
        
        public void setTeam(Team team) {
                // 최초 값 할당 시 null 체크
                if (this.team != null) {
                        team.getMembers().remove(this);
                }
                this.team = team;
                // 내가 이미 저장되어 있는지 (루프 체크)
                if (!team.getMembers().contains(this)) {
                        team.getMembers().add(this);
                }
        }
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {
        @Id
        @GeneratedValue
        @Column(name = "team_id")
        private String id;
        private String name;
        
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
        
        public void addMember(Member member) {
                this.members.add(member);
                // 내가 이미 연관관계 맺고 있는지 (루프 방지)
                if (member.getTeam() != this) {
                        member.setTeam(this);;
                }
        }
}

 

  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
    JPA
    외래 키를 관리할 연관관계의 주인만 사용한다.
    주인이 아닌 쪽은 조희를 위한 JPQL이나 편의 목적으로 객체 그래프 탐색할 사용한다.
  • 양방향 연관관계는 항상 서로 참조해야 한다.
    서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋다.
    편의 메소드는 한쪽/양쪽 작성에 작성하면 된다.
    양쪽에 작성할 경우 순환 호출로 루프에 빠지는 것을 주의해야 한다.

 

일대다

이상의 엔티티를 참조할 있으므로 자바 컬렉션 프레임워크를 사용해 표현한다.

Collection, List, Set, Map

 

일대다 단방향

JPA 2.0 부터 관계 표현 가능

보통 자신이 매핑한 테이블에 외래 키를 관리하는데

경우 반대편 테이블 외래 키를 관리한다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @Column(name = "member_id")
        private String id;
        private String username;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {


        @Id
        @Column(name = "team_id")
        private String id;
        private String name;
        
        @OneToMany
        @JoinColumn(name = "team_id") // Member 테이블의 team_id 컬럼을 의미함
        private List<Member> members = new ArrayList<>();
}

 

일다대 단방향 관계를 매핑할 @JoinColumn 명시하지 않으면, JPA 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 사용한다.

 

 

 

일대다 단방향 매핑의 단점

일쪽 엔티티가 자기가 매핑된 테이블이 아닌 다른 테이블에 외래 키를 관리한다는 것에서 발생한다.

본인 엔티티를 저장하는 본인 엔티티 INSERT SQL 만으로 끝나는게 아니라

외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL 추가로 실행해야 한다.



        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setUsername("m1");
                        Member m2 = new Member();
                        m2.setUsername("m2");
                        
                        Team t1 = new Team();
                        t1.getMembers().add(m1);
                        t1.getMembers().add(m2);
                        
                        em.persist(m1);
                        em.persist(m2);
                        em.persist(t1); // 문제 지점
                });
        }

 

 

Member 엔티티는 Team 엔티티를 모른다.

그러나 연관관계에 대한 정보는 Team.members 관리한다.

따라서 Member 엔티티를 저장할 때는 외래 정보가 빠지고 저장한다.

나중에 Team 엔티티를 저장할 Team.members 참조 값을 확인하고 Member 테이블의 외래 정보를 업데이트한다.

 

 

일대다 양방향

일대다 양방향 매핑은 없다.

양방향 매핑에서 @OneToMany 연관관계의 주인이 없다.

RDB 특성상 일대다 관계에선 외래 키는 항상 쪽에 있다.

 

 

 

일대다 : 다대일 관계에서 항상 다대일 쪽이 연관관계 주인일 밖에 없다.

그런 이유로 ManyToOne mappedBy 속성이 없다.

 

억지로 사용할려면 사용할 있다.



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToOne
        @JoinColumn(name = "team_id",
                insertable = false, updatable = false)
        private Team team;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {


        @Id
        @GeneratedValue
        @Column(name = "team_id")
        private Long id;
        private String name;
        
        @OneToMany
        @JoinColumn(name = "team_id") // Member 테이블의 team_id 컬럼을 의미함
        private List<Member> members = new ArrayList<>();
}

 

일대다 단방향 매핑에 반대편에 다대일 단방향 매핑을 "읽기전용"으로 추가했다.

 

일대일

양쪽이 하나의 관계만 가진다.

 

테이블 관계에서 일대일은 테이블, 대상 테이블 어느 곳이나 외래 키를 가질 있다. 따라서 어느 테이블이 외래 키를 가질지 선택해야 한다.

 

테이블에 외래

객체지향적 방법

테이블에 외래 키를 두고 대상 테이블을 참조한다.

방법은 테이블이 외래 키를 가지고 있으므로 테이블만 확인해도 대상 테이블과 연관관계가 있는지 있다.

 

대상 테이블에 외래

일반적인 DB 관계 사용법

대상 테이블에 외래 키를

방법은 테이블 관계를 일대일에서 일대다로 변경할 테이블 구조를 그대로 유지할 있다.

 

테이블에 외래

테이블에 외래 키에 있어 JPA 쉽게 매핑할 있다.

객체지향에선 객체가 대상 객체의 참조를 가지고 있어야 탐색할 있다.

 

단방향

일대일 관계라 외래키에 유니크 제약조건까지 추가했다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @OneToOne
        @JoinColumn(name = "locker_id")
        private Locker locker;
        private String username;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Locker {
        @Id
        @GeneratedValue
        @Column(name = "locker_id")
        private Long id;
        private String name;
}

 

양방향



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Locker {
        @Id
        @GeneratedValue
        @Column(name = "locker_id")
        private Long id;
        private String name;
        @OneToOne(mappedBy = "locker")
        private Member member;
}

 

연관관계의 주인만 정해주면 된다.

 

대상 테이블에 외래

단방향

일대일 관계 엔티티에서 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.

매핑할 있는 방법이 없다. 하려면 양방향으로 바꿔야 한다.

 

 

양방향

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @OneToOne(mappedBy = "member")
        private Locker locker;
        private String username;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Locker {
        @Id
        @GeneratedValue
        @Column(name = "locker_id")
        private Long id;
        private String name;
        @OneToOne
        @JoinColumn(name = "member_id")
        private Member member;
}

일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면, 양방향으로 매핑해야 한다.

엔티티인 Member 엔티티 대신 대상 엔티티 Locker 연관관계의 주인으로 만들어 Locker 외래 member_id 관리하게 한다.

 

 

다대다

RDB 정규화된 테이블로 다대다 관계를 표현할 없다.

그래서 중간에 연결 테이블을 추가해 일대다-다대일, 일대다-다대일로 해소한다.

 

 

 

다대다 관계는 테이블에서는 불가능하지만, 객체 관계에서는 가능하다.

양쪽에서 컬렉션을 사용하면 그만이다.

 

다대다 단방향



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToMany
        @JoinTable(name = "member_product",
                        joinColumns = @JoinColumn(name = "member_id"),
                        inverseJoinColumns = @JoinColumn(name = "product_id"))
        private List<Product> products = new ArrayList<>();
}

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
}

 



public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                save();
                find();
                emf.close();
        }
        


        private static void find() {
                logic(em -> {
                        Member m1 = em.find(Member.class, 1L);
                        System.out.println(m1);
                });
        }


        private static void save() {
                logic(em -> {
                        Product p1 = new Product();
                        p1.setName("제품1");
                        em.persist(p1);
                        Product p2 = new Product();
                        p2.setName("제품2");
                        em.persist(p2);
                        
                        Member m1 = new Member();
                        m1.setUsername("멤버1");
                        m1.getProducts().add(p1);
                        m1.getProducts().add(p2);
                        em.persist(m1);
                });
        }




        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        logic.accept(em);
                        
                        tx.commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        tx.rollback();
                } finally {
                        em.close();
                }
        }
}

 

저장

 

조회

 

 

 

 

다대다 양방향



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToMany
        @JoinTable(name = "member_product",
                        joinColumns = @JoinColumn(name = "member_id"),
                        inverseJoinColumns = @JoinColumn(name = "product_id"))
        private List<Product> products = new ArrayList<>();
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
        
        @ManyToMany(mappedBy = "products")
        @ToString.Exclude // toString 무한 루프 방지
        private List<Member> members;
}

 

다대다 매핑의 한계와 극복, 연결 엔티티 사용

@ManyToMany 연결 테이블을 자동으로 처리할 있지만, 실무에서 사용하기엔 한계가 있다.

실무라면, 연결 테이블에 멤버ID, 상품ID 끝나지 않는다.

추가 정보가 담긴다.

 

@ManyToMany 로는 추가 정보를 담을 없다.

따로 연결 테이블을 매핑하는 연결 엔티티를 만들고, 거기에 추가 컬럼을 매핑해야 한다.

사실 지금 DDL AUTO 기능은 실무에선 쓰지 못한다.

신규 기능 개발할 때나 독립적으로 사용한다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @OneToMany(mappedBy = "member")
        private List<MemberProduct> memberProducts = new ArrayList<>();
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
}


@Entity
@IdClass(MemberProductId.class)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberProduct {
        
        @Id
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        
        @Id
        @ManyToOne
        @JoinColumn(name = "product_id")
        private Product product;
        
        private int orderAmount;
        private LocalDateTime orderDate;
}
@EqualsAndHashCode
public class MemberProductId implements Serializable{
        //IDE 자동 생성 기능
        private static final long serialVersionUID = 938018879231292890L;
        private Long member;
        private Long product;
}

 

 

 

JPA에서 복합 키를 사용하려면 별도 식별자 클래스 만들어야 한다.

@IdClass 식별자 클래스를 지정한다.

 

복합키를 위한 식별자 클래스 특징

  • 복합 키는 별도 식별자 클래스 필요
  • Serializable 구현
  • EqualsAndHashCode 구현
  • 기본 생성자 필수
  • public 클래스일
  • @IdClass @EmbeddedId 가능

 

식별 관계

MemberProduct Member Product 기본 키를 받아서 자신의 기본 키로 사용한다.

부모 테이블의 기본 키를 받아서 자신의 기본 + 외래 키로 사용하는 것을 DB 용어로 식별 관계라 한다.

 

 

 



        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setUsername("멤버1");
                        em.persist(m1);
                        
                        Product p1 = new Product();
                        p1.setName("상품1");
                        em.persist(p1);
                        
                        MemberProduct memberProduct = new MemberProduct();
                        memberProduct.setMember(m1);
                        memberProduct.setProduct(p1);
                        memberProduct.setOrderAmount(10);
                        memberProduct.setOrderDate(LocalDateTime.now());
                        em.persist(memberProduct);
                        
                });
        }

 

 



        private static void find() {
                logic(em -> {
                        MemberProductId memberProductId = new MemberProductId();
                        memberProductId.setMember(1L);
                        memberProductId.setProduct(1L);
                        
                        MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
                        Member member = memberProduct.getMember();
                        Product product = memberProduct.getProduct();
                        
                        System.out.println(member.getUsername());
                        System.out.println(product.getName());
                });
        }

 

 

복합키로 조회하려면 항상 식별자 클래스를 만들어야 한다.

기본키로 조회하는 것보다 번거롭다.

 

다대다 새로운 기본 사용

복합키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법

 

새로운 기본 생성 전략은 DB에서 자동으로 생성해주는 대리 키는 사용하는 것을 추천

어떠한 비즈니스에 의존하지 않으며, 간편하고 거의 영구적이다.

ORDER_ID 키본 키로 잡고, MEMBER_ID, PRODUCT_ID 외래 키로만 사용한다.

참고, Mysql 예약어 문제로 Orders 끝에 s 붙였다.

 

 

 



@Entity
@Table(name = "Orders")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
        
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        
        @ManyToOne
        @JoinColumn(name = "product_id")
        private Product product;
        
        private int orderAmount;
        private LocalDateTime orderDate;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
}


        private static void find() {
                logic(em -> {
                        Order memberProduct = em.find(Order.class, 1L);
                        Member member = memberProduct.getMember();
                        Product product = memberProduct.getProduct();
                        
                        System.out.println(member.getUsername());
                        System.out.println(product.getName());
                });
        }


        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setUsername("멤버1");
                        em.persist(m1);
                        
                        Product p1 = new Product();
                        p1.setName("상품1");
                        em.persist(p1);
                        
                        Order memberProduct = new Order();
                        memberProduct.setMember(m1);
                        memberProduct.setProduct(p1);
                        memberProduct.setOrderAmount(10);
                        memberProduct.setOrderDate(LocalDateTime.now());
                        em.persist(memberProduct);
                        
                });
        }

복합 키를 사용하는 것보다, 엔티티 저장 조회가 간결해 졌다.

 

 

 

다대다 연관관계 정리

다대다 관계를 일대다 다대일 관계로 풀기 위한 연결 테이블을 만들 식별자를 어떻게 가져갈지 결정해야 한다.

  • 식별 관계
    받아온
    식별자를 기본 + 외래 키로 사용
  • 비식별 관계 (추천)
    받아온 식별자는 외래 키로만 사용, 새로운 식별자 추가

'개발 > JPA' 카테고리의 다른 글

08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06
05 연관관계 매핑 기초  (1) 2024.03.22
04 엔티티 매핑  (1) 2024.02.26
03 영속성 관리  (1) 2024.02.19

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

ORM 핵심은 객체의 참조와 테이블의 외래키를 매핑하는 것이다.

객체는 참조로 관계를 맺고, 테이블은 외래키로 관계를 맺으니

객체의 참조와 테이블의 외래키를 매핑만 잘하면, 개발자는 객체만 다루고, JPA 나머지 부분을 처리해 준다.

 

핵심 키워드

  • 방향
    단방향
    , 양방향

 

 

 

  • 다중성
    일대다
    , 다대일, 다대다, 일대일
  • 연관관계의 주인
    객체의
    경우 연관관계로 만들면 주인이 필요하다

 

 

단방향 연관관계

관계 정의

  • 회원과
  • 회원은 하나의 팀만 소속
  • 회원과 팀은 다대일

 

객체 연관관계

 

테이블 연관관계

 

객체는 참조로 연관관계를 맺는다.

Member.team

멤버는 참조로 팀을 있지만, 반대인 팀은 멤버를 알지 못한다.

 

테이블은 외래 키로 연관관계를 맺는다.

멤버와 테이블은 양방향 관계다.

외래키 TEAM_ID 통해 멤버와 팀을 조인, 반대로 팀과 멤버를 조인 있다.

 

객체에서 양방향을 만드려고 Team Member 참조를 있다.

하지만 이는 양방향 관계가 아닌 단방향 관계를 사용한 것이다.

테이블은 하나의 외래 키로 양방향 조인을 있다.

 

 

 

순수한 객체 연관관계



public class Member {
        private String id;
        private String username;
        
        private Team team;


        public String getId() {
                return id;
        }


        public void setId(String id) {
                this.id = id;
        }


        public String getUsername() {
                return username;
        }


        public void setUsername(String username) {
                this.username = username;
        }


        public Team getTeam() {
                return team;
        }


        public void setTeam(Team team) {
                this.team = team;
        }
}


public class Team {
        private String id;
        private String name;
        
        public String getId() {
                return id;
        }
        public void setId(String id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
}


Member member = new Member();
member.setId("m1");
member.setUsername("회원1");


Team team = new Team();
team.setId("t1");
team.setName("팀1");


member.setTeam(team);

객체는 참조를 통해 연관관계를 맺는다.

 

 

테이블 연관관계



CREATE TABLE `MEMBER` (
  `MEMBER_ID` VARCHAR(255) NOT NULL,
  `TEAM_ID` VARCHAR(255),
  `USERNAME` VARCHAR(255),
  PRIMARY KEY (`MEMBER_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
;
CREATE TABLE `TEAM` (
  `TEAM_ID` VARCHAR(255) NOT NULL,
  `NAME` VARCHAR(255),
  PRIMARY KEY (`TEAM_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
;
ALTER TABLE `MEMBER` ADD CONSTRAINT `FK_MEMBER_TEAM`
  FOREIGN KEY (`TEAM_ID`) REFERENCES `TEAM` (`TEAM_ID`);


SELECT A.*,
       B.*
  FROM MEMBER A
  JOIN TEAM B
    ON A.TEAM_ID = B.TEAM_ID ;

데이터 베이스는 외래키를 통해 연관관계를 맺는다.

 

객체 관계 매핑

참고, 롬복 사용

@Entity
@Data
public class Member {
        @Id
        @Column(name = "MEMBER_ID")
        private String id;
        private String username;
        
        //연관관계 매핑
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
}


@Entity
@Data
public class Team {
        @Id
        @Column(name = "TEAM_ID")
        private String id;
        private String name;
}

 

 

객체 연관관계, Member.team

테이블 연관관계 MEMBER.TEAM_ID

 

@ManyToOne

어노테이션은 엔티티 객체에 테이블의 관계를 표현해준다.

그대로 N:1 관계를 표현해준다.

@JoinColumn(name = "TEAM_ID")

"외래 " 매핑할 사용한다. , DB 컬럼 이름을 인자로 받는다.

어노테이션은 생략이 가능하다.

 

@JoinColumn

 

속성 기능 기본값
name 매핑할 외래 이름 클래스 필드명 + _ + 테이블 기본 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 컬럼명
foreignKey 테이블 생성이 적용될 때 외래 키 제약 조건 생성을 지정하거나 제어하는 ​​데 사용
나머지 속성은 @Column 같다.

 



@Entity
@Data
public class Member {
        @Id
        @Column(name = "MEMBER_ID")
        private String id;
        private String username;
        
        //연관관계 매핑
        @ManyToOne
//        @JoinColumn(name = "TEAM_ID")
        private Team team;
}

 

Member.team 필드명 + _ + Team테이블 기본키명

 

 

 

@ManyToOne

속성 기능 기본값
optional false 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 패치 전략 설정,  지연 로드해야 하는지 아니면 즉시 가져와야 하는지 여부 FetchType.EAGER
어노테이션 마다 다름
CascadeType 영속성 전이를 사용 타입 결정
targetEntity 연관된 엔티티 클래스 타입 정보

 

targetEntity 컬렉션을 Raw 타입으로 다룰 필요하다. 하지만 지네릭을 사용하므로 사용할 일이 없다.

 

 

 

연관관계 사용

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @Column(name = "MEMBER_ID")
        private String id;
        private String username;
        
        //연관관계 매핑
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;


        public Member(String id, String username) {
                this.id = id;
                this.username = username;
        }
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {
        @Id
        @Column(name = "TEAM_ID")
        private String id;
        private String name;
}

 



public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                
        }
        
        
        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        logic.accept(em);
                        
                        tx.commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        tx.rollback();
                } finally {
                        em.close();
                }
        }
}

중복 제거를 위해 위와 같이 따로 메서드를 생성



        private static void save() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1");
                        em.persist(t1);
                        
                        Member m1 = new Member("member1", "멤버1");
                        m1.setTeam(t1);
                        em.persist(m1);
                        
                        Member m2 = new Member("member2", "멤버2");
                        m2.setTeam(t1);
                        em.persist(m2);
                });
        }

멤버 엔티티에 참조를 넣으면, 참조의 Id 값을 읽어서 바인딩 해준다.

 

 

 

 

 

 

조회

        private static void find() {
                logic(em -> {
                        Member m1 = em.find(Member.class, "member1");
                        Team t1 = m1.getTeam(); // 객체 그래프 탐색
                        System.out.println(m1);
                        System.out.println(t1);
                });
        }

 

 

 

엔티티로 연관된 엔티티를 조회하는 것은 객체 그래프 탐색이라 한다.

멤버 조회 연관된 엔티티 팀을 별도 DB 조회 없이 자바 객체 처럼 꺼내서 사용하는 것을 있다.

 

 

조회 JPQL



        private static void find2() {
                logic(em -> {
                        String query = "select m from Member m join m.team t "
                                        + "where t.name = :teamName";
                        TypedQuery<Member> q = em.createQuery(query, Member.class)
                                        .setParameter("teamName", "팀1");
                        List<Member> resultList = q.getResultList();
                        System.out.println(resultList);
                        
                });
        }

 

 

 

조회 조회하는 것을 있다.

 

 

fetch 붙이면 추가 질의 없이 번에 끝난다.

 

 

연관관계 제거



        private static void remove() {
                logic(em -> {
                        Member m1 = em.find(Member.class, "member1");
                        m1.setTeam(null); // 연관관계 제거
                        // em.persist 가 없어도, 더티체킹으로 감지되어 제거된다.(Update)
                });
        }

 

 

 

 

연관된 엔티티 삭제



        private static void remove2() {
                logic(em -> {
                        Member m1 = em.find(Member.class, "member1");
                        Member m2 = em.find(Member.class, "member2");
                        
                        m1.setTeam(null);
                        m2.setTeam(null);
                        
                        // 팀을 제거하려면 기존 연관관계를 모두 끊어야 가능하다.
                        em.remove(em.find(Team.class, "team1"));
                });
        }

 

 

 

양방향 연관관계

테이블은 수정할 것이 없다.

외래 하나로 원래부터 양방향으로 조회할 있다.

객체는 양방향을 흉내내기 위해 단방향을 하나 추가한 것이다.

멤버 ->

-> 멤버

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {
        @Id
        @Column(name = "TEAM_ID")
        private String id;
        private String name;
        
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
}

일대다 표현을 위해 컬렉션을 사용

mappedBy 속성은 양방향 매핑일 사용하는데 반대쪽 매핑의 필드 이름을 값으로 준다.

 



        private static void find() {
                logic(em -> {
                        Team t1 = em.find(Team.class, "team1");
                        for (Member m : t1.getMembers()) {
                                System.out.println(m.getUsername());
                        }
                });
        }




        private static void save() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1", null);
                        em.persist(t1);
                        
                        Member m1 = new Member("member1", "멤버1");
                        m1.setTeam(t1);
                        em.persist(m1);
                        
                        Member m2 = new Member("member2", "멤버2");
                        m2.setTeam(t1);
                        em.persist(m2);
                });
        }

toString() 호출하면 안된다. 순환 참조 문제로 StackOverflowError 발생한다.

 

연관관계의 주인

mapperBy 속성의 필요성

 

테이블은 외래 하나로 테이블 연관관계 관리

객체는 방향 개로 양방향을 흉내낸다.

, 객체에는 참조가 둘인데 외래 키는 하나다. 이런 차이가 발생한다.

객체 쪽이 테이블의 외래 키는 관리하고, 이를 연관관계의 주인이라 한다.

 

양방향 매핑의 규칙: 연관관계의 주인

 

연관관계의 주인만이 DB 연관관계와 매핑되고 외래 키를 CUD 있다.

주인이 아닌 쪽은 읽기만 가능하다.

연관관계 주인을 정하는 속성이 mappedBy 이다.

 

주인이 아닌 경우 mappedBy 속성을 사용해 연관관계의 주인을 명시한다.

 

연관관계의 주인을 정한다는 것은 외래 관리자를 선택하는 것이다.

 

예시로는 누가 TEAM_ID 외래 키를 관리할지 정해야 한다.

Member.team 영우 자신의 매핑된 테이블에 TEAM_ID 관리하면 되니 자연스럽다.

 

Tema.members 경우 자신의 테이블에 없는 MEMBER 테이블의 TEAM_ID 관리해야 한다. 불가능한 경우이다.

 

, 연관관계의 주인은 테이블에 외래 키가 있는 곳이다.

Member.team 연관관계의 주인이다.

 

연관 관계의 주인만 외래 키에 대한 CUD 가능하고,

주인이 아닌 반대편은 외래 읽기만 가능하다.

 

일대다, 다대일 연관관계에서는 항상 쪽이 외래 키를 가지게 된다.

다대일을 표현하는 @ManyToOne 항상 연관관계의 주인이므로 mappedBy 속성이 존재하지 않는다.

 

 

양방향 연관관계 저장

연관관계의 주인



        private static void save() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1");
                        em.persist(t1);
                        
                        Member m1 = new Member("member1", "멤버1");
                        m1.setTeam(t1);
                        em.persist(m1);
                        
                        Member m2 = new Member("member2", "멤버2");
                        m2.setTeam(t1);
                        em.persist(m2);
                });
        }

 

 

양방향 연관관계 주의점

연관관계의 주인이 아님



        private static void save2() {
                logic(em -> {
                        Member m1 = new Member("member1", "멤버1");
                        em.persist(m1);
                        
                        Member m2 = new Member("member2", "멤버2");
                        em.persist(m2);
                        //연관관계의 주인이 아닌곳에서 저장
                        Team t1 = new Team("team1", "팀1");
                        t1.getMembers().add(m1);
                        t1.getMembers().add(m2);
                        em.persist(t1);
                });
        }

 

 

 

순수한 객체까지 고려한 양방향 연관관계

객체 관점에서 양쪽 방향에서 모두 값을 입력해주는 것이 안전하다.

JPA 자바 ORM 표준으로 객체와 RDB 모두 중요하다.

 



        private static void save3() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1");
                        em.persist(t1);
                        
                        Member m1 = new Member("member1", "멤버1");
                        // 연관관계의 주인, 저장 시 사용된다.
                        m1.setTeam(t1);
                        // 저장 시에는 사용되지 않으나 애플리케이션에선 사용된다.
                        t1.getMembers().add(m1);
                        em.persist(m1);
                        
                        Member m2 = new Member("member2", "멤버2");
                        // 연관관계의 주인, 저장 시 사용된다.
                        m2.setTeam(t1);
                        // 저장 시에는 사용되지 않으나 애플리케이션에선 사용된다.
                        t1.getMembers().add(m2);
                        em.persist(m2);
                });
        }

 

연관관계 편의 메소드

 

양방향 연관관계는 기존 단방향에 더해 추가 단방향까지 신경써야한다.

전부 호출해야 양방향이 된다.

실수로 하나란 호출하면 양방향이 깨진다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @Column(name = "MEMBER_ID")
        private String id;
        private String username;
        
        //연관관계 매핑
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;


        public Member(String id, String username) {
                this.id = id;
                this.username = username;
        }
        
        public void setTeam(Team team) {
                this.team = team;
                team.getMembers().add(this);
        }
}

 

연관관계의 주인 엔티티에 번만 호출해도 나머지도 처리되도록 편의 기능을 추가했다.

        // 연관관계 편의 메서드 리팩터링 저장
        private static void save4() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1");
                        em.persist(t1);
                        
                        Member m1 = new Member("member1", "멤버1");
                        // 연관관계의 주인, 저장 시 사용된다.
                        m1.setTeam(t1);
                        // 저장 시에는 사용되지 않으나 애플리케이션에선 사용된다.
                        // t1.getMembers().add(m1);
                        em.persist(m1);
                        
                        Member m2 = new Member("member2", "멤버2");
                        // 연관관계의 주인, 저장 시 사용된다.
                        m2.setTeam(t1);
                        // 저장 시에는 사용되지 않으나 애플리케이션에선 사용된다.
                        // t1.getMembers().add(m2);
                        em.persist(m2);
                });
        }

번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.

 

연관관계 편의 메소드 작성 주의사항



        // 연관관계 편의 메서드 주의사항
        private static void save5() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1");
                        em.persist(t1);
                        Team t2 = new Team("team2", "팀2");
                        em.persist(t2);
                        
                        Member m1 = new Member("member1", "멤버1");
                        m1.setTeam(t1);
                        m1.setTeam(t2);
                        em.persist(m1);
                        
                        // t1 은 여전히 멤버1을 가지고 있다. 물론
                        // 연관관계의 주인이 아니라 DB에 영향은 없지만,
                        // 문제가 있다.
                        t1.getMembers().forEach(m -> {
                                System.out.println(m.getUsername());
                        });
                        
                });
        }

 

 

team1 -> member1 연관관계 편의 메서드가 처리한

연관관계 편의 메서드에서 신규 관계만 집중한 나머지 남겨질 관계를 신경 쓰지 못한 모습



        public void setTeam(Team team) {
                // 기존 팀에 나의 관계를 지움
                if (this.team != null) {
                        this.team.getMembers().remove(this);
                }
                
                this.team = team;
                //편의 메서드
                team.getMembers().add(this);
        }


        // 연관관계 편의 메서드 주의사항
        private static void save5() {
                logic(em -> {
                        Team t1 = new Team("team1", "팀1");
                        em.persist(t1);
                        Team t2 = new Team("team2", "팀2");
                        em.persist(t2);
                        
                        Member m1 = new Member("member1", "멤버1");
                        m1.setTeam(t1);
                        m1.setTeam(t2);
                        em.persist(m1);
                        
                        // t1 은 여전히 멤버1을 가지고 있다. 물론
                        // 연관관계의 주인이 아니라 DB에 영향은 없지만,
                        // 문제가 있다.
                        System.out.println("##################");
                        t1.getMembers().forEach(m -> {
                                System.out.println(m.getUsername());
                        });
                        
                });
        }

 

 

지금 발생한 모든 문제는 객체에서 서로 다른 단방향 연관관계 2개를 이용해서 양방향을 흉내내기 위해서 많은 수고가 필요하다는 것을 알려주고 있다.

 

따라서, 필요한 경우에만 객체 시점 양방향 관계를 사용하도록 한다.

가급적 단방향만을 사용한다.

 

정리

단방향 매핑보다 양방향이 훨씬 복잡하다.

연관관계의 주인을 정해야 하고, 양방향 매핑 관련 로직을 신경써야 한다.

 

양방향의 장점은 엔티티 사이 양쪽으로 그래프 탐색 기능이 추가된 것에 불과하다.

따라서 양방향 그래프 탐색이 필요하지 않다면, 단방향만 사용하도록 한다.

 

연관관계의 주인은 항상 외래키가 있는 쪽이다. 비즈니스 데이터가 중요한 쪽을 선택하는 것이 아니다.

 

실전 예제

연관관계 매핑 시작

참고, 롬복 사용

@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;
        
        private String name;
        private String city;
        private String strret;
        private String zipcode;
        
        @OneToMany(mappedBy = "member")
        @ToString.Exclude
        private List<Order> orders = new ArrayList<>();
}


@Entity
@Table(name = "ORDERS")
@Data
public class Order {
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ID")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "MEMBER_ID")
        private Member member;
        
        @OneToMany(mappedBy = "order")
        private List<OrderItem> orderItems = new ArrayList<>();
        
        private LocalDateTime orderDate;
        
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
        // 연관관계 편의 메소드
        public void setMember(Member member) {
                if (this.member != null) {
                        this.member.getOrders().remove(this);
                }
                this.member = member;
                member.getOrders().add(this);
        }
        public void addOrderItem(OrderItem orderItem) {
                orderItems.add(orderItem);
                orderItem.setOrder(this);
        }
}


public enum OrderStatus {
        ORDER, CANCEL
}


@Entity
@Table(name = "ORDER_ITEM")
@Data
public class OrderItem {
        
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ITEM_ID")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "ITEM_ID")
        private Item item;


        @ManyToOne
        @JoinColumn(name = "ORDER_ID")
        private Order order;
        
        private int orderPrice;
        private int count;
        
        public void setOrder(Order order) {
                if (this.order != null) {
                        this.order.getOrderItems().remove(this);
                }
                this.order = order;
                order.getOrderItems().add(this);
        }
}


@Entity
@Data
public class Item {
        @Id
        @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
        
        private String name;
        private int price;
        private int stockQuantity;
}

 

 

 

 

 

 

'개발 > JPA' 카테고리의 다른 글

07 고급 매핑  (0) 2024.05.06
06 다양한 연관관계 매핑  (0) 2024.04.14
04 엔티티 매핑  (1) 2024.02.26
03 영속성 관리  (1) 2024.02.19
02 JPA 소개  (0) 2023.12.01

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

  • 객체 테이블 매핑
    @Entity, @Table
  • 기본 매핑
    @Id
  • 필드 컬럼 매핑
    @Column
  • 연관고나계 매핑
    @ManyToOne( 다수), @JoinColumn

 

매핑은 XML이나 애노테이션 하나로 기술한다.

요즘은 거의 애노테이션만 사용하는 추세다.

 

@Entity

필수 어노테이션, 값을 기준으로 JPA 관리할 엔티티를 식별한다.

 

필수 규칙

  • protect 범위 이상 기본 생성자 필수
  • final 클래스, enum, interface, inner 클래스에 사용 불가
  • 저장 필드에 final 사용 불가

 

기본 생성자 규칙은 쉽게 실수 있는 부분이다.



@Entity
public class Member {
        @Id
        @Column(name = "MEMBER_ID")
        private Long id;
        private String name;
        
        // 생성자가 아무 것도 없으면, 컴파일러가 기본 생성자를 생성해준다.
        // 이렇게 생성자를 만들면, 컴파일러는 기본 생성자를 안만들어 준다.
        public Member(Long id, String name, Team team) {
                this.id = id;
                this.name = name;
                this.team = team;
        }

다른 곳에서도 기본 생성자는 필수 때가 있다. 무조건 기본 생성자는 만들어도 나쁠 없다.

 

@Table

엔티티와 테이블을 매핑할 사용한다.

생략이 가능하다. 생략 @Entity 붙은 클래스 이름과 같은 테이블에 매핑한다.

속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름
catalog catalog 있는 DB에서 catalog 매핑
schema schema 있는 DB에서 schema 매핑
uniqueConstraints 유니크 제약조건을 추가한다. 자동 DDL 생성에서 사용한다. 실제 실무에선 Table 직접 만드므로 사용할 일이 없다.

 

매핑 정보



import java.util.Date;


import jakarta.persistence.*;


@Entity
@Table(name = "MEMBER")
public class Member {
        @Id
        @Column(name = "ID")
        private String id;
        
        @Column(name = "NAME")
        private String username;
        
        private Integer age;
        
        @Enumerated(EnumType.STRING)
        private RoleType roleType;
        
        @Temporal(TemporalType.TIMESTAMP)
        private Date createdDate;
        
        private Date lastModifiedDate;
        
        @Lob
        private String description;


        public String getId() {
                return id;
        }


        public void setId(String id) {
                this.id = id;
        }


        public String getUsername() {
                return username;
        }


        public void setUsername(String username) {
                this.username = username;
        }


        public Integer getAge() {
                return age;
        }


        public void setAge(Integer age) {
                this.age = age;
        }


        public RoleType getRoleType() {
                return roleType;
        }


        public void setRoleType(RoleType roleType) {
                this.roleType = roleType;
        }


        public Date getCreatedDate() {
                return createdDate;
        }


        public void setCreatedDate(Date createdDate) {
                this.createdDate = createdDate;
        }


        public Date getLastModifiedDate() {
                return lastModifiedDate;
        }


        public void setLastModifiedDate(Date lastModifiedDate) {
                this.lastModifiedDate = lastModifiedDate;
        }


        public String getDescription() {
                return description;
        }


        public void setDescription(String description) {
                this.description = description;
        }
}
public enum RoleType {
        ADMIN, USER
}

 

DB 스키마 자동 생성

persistence.xml 설정 정보



<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
    version="3.0">
    <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- DB 연결 정보 -->
            <property name="jakarta.persistence.jdbc.driver"
                value="com.mysql.cj.jdbc.Driver" />
            <property name="jakarta.persistence.jdbc.url"
                value="jdbc:mysql://localhost:55555/jpa?characterEncoding=utf8" />
            <property name="jakarta.persistence.jdbc.user" value="root" />
            <property name="jakarta.persistence.jdbc.password" value="root" />
            <property name="hibernate.hikari.poolName" value="pool" />
            <property name="hibernate.hikari.maximumPoolSize" value="10" />
            <property name="hibernate.hikari.minimumIdle" value="10" />
            <property name="hibernate.hikari.connectionTimeout" value="1000" />
            <!--  MySql 방언 -->
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.MySQLDialect" />
            <!-- SQL 출력 -->
<!--            <property name="hibernate.show_sql" value="true" />-->
            <!-- SQL 이쁘게 출력 -->
            <property name="hibernate.format_sql" value="true" />
            <!-- 주석도 함께 출력 -->
            <property name="hibernate.use_sql_comments" value="true" />
            <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 -->
            <property name="hibernate.id.new_generator_mappings" value="true" />
            <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>

 

hibernate.hbm2ddl.auto 옵션을 키면, @Entity 정보를 기반으로 DDL 생성한다.

DDL hibernate.dialect 속성에 설정한 방언에 따라 DB 맞게 생성된다.

 

DDL 자동 생성 기능은 개인적으로 학습할 때나, 신규 기능으로 테이블은 만들기 전에 테스트 용도로만 사용해야 한다.

잘못하면, 개발DB 운영 DB 전부 날릴 수도 있다.

 

옵션 설명
create 기존 테이블 삭제, 새로 생성
create-drop create 기능 + 종료 생성된 DDL 제거
update DB 테이블과 엔티티 비교 변경 분만 수정
validate DB 테이블과 엔티티를 비교 다르면, 실행을 하지 않는다
none 사용 안함, 속성 값을 지우는 것과 동일

 

이름 매핑 전략 변경

애플리케이션은 일반적으로 카멜케이스 표기법을 DB 언더스코어 표기법을 많이 사용한다. 이를 자동으로 매핑해주는 속성이 있다.

 

과거 org.hibernate.cfg.NamingStrategy 속성으로 엔티티와 테이블 이름 매핑 전략을 설정했다. 현재는

hibernate.implicit_naming_strategy, hibernate.physical_naming_strategy 분리됐다.

implicit 애플리케이션 , physical DB

분리가 됐다는 것은 과서 org.hibernate.cfg.NamingStrategy 설정이 개의 책임을 가졌다는 것을 의미한다. 현재는 제거된 속성이다.

https://docs.jboss.org/hibernate/orm/5.0/userguide/html_single/Hibernate_User_Guide.html#naming

 

 

 

 

DDL 생성 기능

어디까지나 자동 DDL 생성 기능을 활용한 것이다.

절대 운영이나 개발에선 사용하면 안된다.

 

 

유니크 제약조건 생성하기

 

 

 

 

주의할 것은 @Column(name = "NAME", nullable = false, length = 10) 에서 nullable, length DDL 자동 생성 사용되는 정보지 JPA 실행 로직에는 영향을 주지 않는다.

 

DDL 자동 생성 기능은 그냥 실제 개발할 사용하지말고 개인적인 학습 용도로 사용하는 것이 좋다.

기본 매핑

  • 직접할당
  • 자동할당

 

자동 생성 방식이 많은 이유는 DB마다 생성 방식이 다르기 때문이다.

오라클 시퀀스, Mysql AUTO_INCREMENT 등…

 

SEQUENCE, IDENTITY는 DB 의존적인 옵션이다

 

자동 생성 전략을 위해선 persistence.xml 파일에 아래와 같은 설정을 반드시 해야 한다.

 

키가 있는 타입

@Id

  • 기본형
  • 기본형의 래퍼 클래스
  • String
  • java.util.Date
  • java.sql.Date
  • java.math.BigDecimal
  • java.math.BigInteger

2023.11 현재도 그대로다.

 

없지만, mysql 기준 enum 된다.

 

 

 

IDENTITY 전략

생성을 DB에게 위임

Mysql

Oracle

 

 

 

전략은 키를 생성하기 위해 무조건 DB 질의를 해야한다.

, 쓰기 지연 SQL 저장되지 않고, 바로 DB INSERT 한다.

영속성 컨텍스트에 저장하기 위해선 무조건 식별자가 필수라는 것을 상기하자

 

SEQUENCE 전략

시퀀스를 사용하여 기본 키를 생성

시퀀스를 지원하는 DB에서 사용한다.

 

 

 

 

 

 

실습 중인 Mysql 시퀀스를 지원 안해 테이블이 만들어진다.

 

오라클로 테스트

 

 

 

persistence.xml



<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
    version="3.0">
    <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- DB 연결 정보 -->
            <property name="jakarta.persistence.jdbc.driver"
                value="oracle.jdbc.OracleDriver" />
            <property name="jakarta.persistence.jdbc.url"
                value="jdbc:oracle:thin:@localhost:54321:XE" />
            <property name="jakarta.persistence.jdbc.user" value="scott" />
            <property name="jakarta.persistence.jdbc.password" value="tiger" />
            <property name="hibernate.hikari.poolName" value="pool" />
            <property name="hibernate.hikari.maximumPoolSize" value="10" />
            <property name="hibernate.hikari.minimumIdle" value="10" />
            <property name="hibernate.hikari.connectionTimeout" value="1000" />
            <!--  Oracle 방언 -->
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.OracleDialect" />
            <!-- SQL 출력 -->
<!--            <property name="hibernate.show_sql" value="true" />-->
            <!-- SQL 이쁘게 출력 -->
            <property name="hibernate.format_sql" value="true" />
            <!-- 주석도 함께 출력 -->
            <property name="hibernate.use_sql_comments" value="true" />
            <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 -->
            <property name="hibernate.id.new_generator_mappings" value="true" />
            <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
<!--            <property name="hibernate.implicit_naming_strategy" value="jpa" />-->
<!--            <property name="hibernate.physical_naming_strategy" -->
<!--                value="org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy" />-->
        </properties>
    </persistence-unit>
</persistence>

 

 

오라클은 시퀀스를 지원해 생성된다.

 

다시 돌아와서 시퀀스 전략의 문제는 다시 영속성 컨텍스트 식별자 필수 제약과 연관있다.

영속성 컨텍스트에 저장되기 위해선 무조건 시퀀스가 있어야 한다.

 

따라서 영속성 컨텍스트에 저장하기 위한 시퀀스 조회를 수행하게 된다.

em.persist() 호출 시점에 시퀀스 조회

영속성 컨텍스트에 저장 INSERT 쿼리는 쓰기 지연 SQL 저장된다.

이후 플러시 시점에 INSERT 쿼리가 실행된다.

 

결과적으로 DB 통신하게 된다. 네트워크 통신은 가장 병목 시점이다.

 

다른 문제로 시퀀스가 50 증가하는 증상은 아래와 같은 설정으로 변경할 있다.

명시적으로 @SequenceGenerator 어노테이션으로 시퀀스 생성기를 등록하고, @GeneratedValue.generator 속성으로 시퀀스 생성기를 선택한다.

 

@SequenceGenerator 속성

속성 기능 기본값
name 시퀀스 생성기 이름 필수
sequenceName DB 시퀀스 이름
initialValue 최초 생성 시작 , 따라서 DDL 자동 생성 사용 사용하는 속성 1
allocationSize 시퀀스 호출 증가하는 50
catalog 카탈로그 지원하는 DB 카탈로그
schema 스키마 지원하는 DB 스키마

 

 

 

Caused by: org.hibernate.MappingException: The increment size of the [MEMBER_SEQ] sequence is set to [20] in the entity mapping while the associated database sequence increment size is [1].
        at org.hibernate.id.enhanced.SequenceStyleGenerator.configure(SequenceStyleGenerator.java:218)
        at org.hibernate.id.factory.internal.StandardIdentifierGeneratorFactory.createIdentifierGenerator(StandardIdentifierGeneratorFactory.java:217)
        ... 19 more

DDL 자동 생성 기능을 꺼도, allocationSize는 사용되는 유효한 값이다.

allocationSize 기본 값이 50 이유

시퀀스 전략은 INSERT DB 통신한다고 했다.

이를 줄이기 위한 최적화 방법으로 시퀀스는 50 증가시킨다.

시퀀스를 조회하고, 다음 시퀀스 증가 크기 만큼 메모리에서 시퀀스를 처리한다.

, 50 INSERT 마다 시퀀스를 조회한다.

 

Member member = null;
for (int i = 0; i < 50; i++) {
        member = new Member();
        member.setUsername("홍길동" + i);
        em.persist(member);
}

 

 

번의 시퀀스 조회로 50 까지 식별자를 메모리에서 할당한 것을 있다.

보통은 allocationSize 값을 1 주고 쓴다.

 

테이블 전략

생성 전용 테이블을 만들고 시퀀스를 흉내낸다.

 

 

 

 

테이블 전략도 allocationSize 속성 값으로 최적화를 진행할 있다.

 

 

 

로우 하나가 하나의 시퀀스 오브젝트를 흉내낸다.

 

 

@TableGenerator

속성 기능 기본값
name 테이블 식별자 생성기 이름 필수
table DB 테이블
pkColumnName 시퀀스 컬럼명, 시퀀스 오브젝트 이름이라고 봐도 된다.
valueColumnValue 시퀀스 컬럼명
initialValue 초기 0
allocationSize 호출 증가값 50
catalog, schema

uniqueConstraints 유니크 제약 조건 지정(테이블 이라 가능한 속성)

 

 

AUTO 전략

선택한 DB 방언에 따라 TABLE, SEQUENCE, IDENTITY 하나를 자동으로 선택

 

 

 

 

참고로 기본 값은 AUTO

 

참고

 

 

기본 매핑 정리

직접 할당 DB 통신은 플러시 시점 번만 이뤄 진다.
SEQUENCE 시퀀스 번을 위해 persist() 시점에 DB 통신, 플러시 시점에 DB 통신
TABLE 시퀀스와 똑같은 이유로 DB 통신
IDENTITY DB에게 위임 Mysql 경우 무조건 데이터 저장시 키가 생성되기에 persit() 시점에 INSERT 이뤄진다.

 

 

기본 조건

  • null 불가
  • 유일성
  • 변하면 안됨

 

기본 선정 전략

  • 자연
    비즈니스
    데이터를 사용한
  • 대리
    비즈니스
    데이터와 관계 없는 임의
    자동
    생성 전략은 전부 대리키다.
     

 

있으면 대리 사용을 권장, 비즈니스는 언젠간 변하게 되어있다.

반면에 대리 키는 변할 여지가 없다.

 

대리 키를 기본 키로 잡고, 자연 후보들로 유니크 인덱스를 잡아서 사용하는 것을 권장한다.

 

필드와 컬럼 매핑

@Column 컬럼 매핑
@Enumerated 자바 열거형 매핑
@Temporal 날짜 타입 매핑
@Lob BLOB, CLOB 매핑
@Transient 매핑 무시할 필드

 

@Column

 

 

 

속성 기능 기본값
name 컬럼 매핑 필드 이름
insertable 필드 저장 가능 여부, 읽기 전용으로 만들 가끔 사용 true
updatable 필드 수정 가능 여부, 읽기 전용으로 만들 가끔 사용 true
table 하나의 엔티티를 이상 테이블에 매핑할 사용 현재 클래스와 매핑된 테이블
nulable DDL 생성 null 가능 여부 true
unique DDL 생성 해당 컬럼의 유니크 제약 조건 설정
2 이상 복합 유니크 제약은 클래스 레벨에서 @Table uniqueConstaints 사용해야 한다. 물론 하나도 가능
false
length DDL 생성 길이 제한, String 타입만 가능하다 255
precision DDL 생성 BigDecimal, BigInteger 에서 사용, 소수점 포함 전체 자리수 38
scale DDL 생성 BigDecimal에서 사용, 소수점 2

 

 

기본 참고

자바 기본형은 전부 자동으로 not null 붙는다.

굳이 null 가능하게 설정할 수도 있다.

하지만, 자바 단에서 기본값이 존재하기 때문에 DB null 절대로 들어갈 없다.

 

이런 불일치 위험 때문에 기본형은 nullable = false 명시적으로 주는 것이 안전하다

아니면 기본형의 래퍼 클래스를 사용하도록 한다.

 

 

@Enumerated

자바 enum 열거형을 매핑할 사용한다.

 

 

기본 값이 ORDINAL 되어 있다.

무조건 STRING으로 사용한다.

 

이유는 간단하다. ORDINAL 말그대로 순서로 순서에 의존하게 된다.

의존성을 없앨 있는 선택지가 있는데 의존성을 굳이 가져가는 것은 좋지 않다.

 

 

@Temporal

날짜 타입을 매핑하기 위해 사용한다.

 

java.sql.Date 년월일 정보만 담을 있다.

java.sql.Time 시분초 정보만 담을 있다.

java.sql.Timestamp 년월일시분초

 

해당 타입으로 매핑은 하더라도, 설정된 정보 만큼만 담긴다.

 

 

java.sql.* 패키지의 날짜 타입만 빼고, 어느 것이든 자기가 있는 사이트에서 사용하는 것을 사용하도록 한다.

@Temporal 생략해도 기본 동작이 "년월일시분초"라는 것만 알고 있으면 된다.

 

 


@Lob

DB BLOB, CLOB 매핑

  • CLOB
    String, char[], java.sql.CLOB
  • BLOB
    byte[], java.sql.BLOB
     

 

 

 

 

 

@Transient

매핑을 무시할 필드를 선정한다.

 

 

 

 

@Access

JPA 엔티티 데이터의 접근하는 방식을 지정한다.

 

 

 

필드 접근 방식은 필드가 private 이여도 상관 없다.

 

 

@Access 명시적으로 없으면, @Id 위치로 접근 방식이 결정된다.

 

@Id 필드에 있으므로, 필드 전략을 사용

 

서로 다른 전략을 사용할 수도 있다.

 

 

실전 예제

코드만 기술

@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;
        
        private String name;
        private String city;
        private String strret;
        private String zipcode;
}
@Entity
@Table(name = "ORDERS")
@Data
public class Order {
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ID")
        private Long id;
        @Column(name = "MEMBER_ID")
        private Long memberId;
        
        private LocalDateTime orderDate;
        
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
}


public enum OrderStatus {
        ORDER, CANCEL
}


@Entity
@Table(name = "ORDER_ITEM")
@Data
public class OrderItem {
        
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ITEM_ID")
        private Long id;
        
        @Column(name = "ITEM_ID")
        private Long itemId;
        @Column(name = "ORDER_ID")
        private Long orderId;
        
        private int orderPrice;
        private int count;
}


@Entity
@Data
public class Item {
        @Id
        @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
        
        private String name;
        private int price;
        private int stockQuantity;
}

 

객체지향설계는 객체가 맡은 역할과 책임을 가지고, 객체 참조를 통해 협업하도록 설계한다.

예제 코드는 객체 설계를 테이블에 맞춘 방법이다. 외래 부분이 그대로 데이터 타입인 것을 있다. 테이블은 문제가 안되는 데이터로 조인을 하면 된다. 하지만 객체는 조인이 불가능하다. 연관된 객체의 참조가 있어야 한다.

'개발 > JPA' 카테고리의 다른 글

06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22
03 영속성 관리  (1) 2024.02.19
02 JPA 소개  (0) 2023.12.01
01 JPA 환경 설정  (0) 2023.11.23

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

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

https://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/img/jpa-arch.png

 

 

엔티티 매니저 팩토리는 스레드 세이프티 하다.

엔티티 매니저는 반대로 절대 다른 스레드 공유하면 안된다.

 

 

대부분 JPA 구현체들은 엔티티매니저팩토리를 만들 커넥션 풀도 같이 만든다.

persistence.xml 설정을 보면 있다.

 

 

 

영속성 컨텍스트(Persistence context)

영속성 그대로 엔티티(데이터) 영구히 저장되는 저장소를 의미한다.

1 캐새라고도 불린다.

영속성 컨텍스트는 엔티티 매니저를 생성할 하나 만들어진다.

엔티티 매니저를 통해 영속성 컨텍스트를 조작할 있다.

하나의 영속성 컨텍스트는 여러 엔티티 매니저가 접근 수도 있다.

 

엔티티 생명주기

 

https://thorben-janssen.com/entity-lifecycle-model/

 

비영속 (Transient/ new)

데이터베이스 및 JPA 관련 기능에 대한 연결이 없는 기본 Java 객체

 

영속(Managed)

모든 변경 사항을 감지하고 지속성 컨텍스트를 플러시할 때 필요한 SQL INSERT 또는 UPDATE 문을 생성

저장된 객체도 영속 상태

조회된 상태도 영속 상태, 참고로 JPQL 조회한 객체도 영속 상태다.

EntityManager.merge 메소드를 호출하여 분리된 엔티티를 병합할 때도 영속 상태가 된다

 

준영속(Detached)

영속성 컨텍스트가 관리하던 엔티티가 관리하지 않게 상태

영속성 컨텍스트에서 특정 엔티티를 분리하거나

영속성 컨텍스트를 초기화하거나

영속성 컨텍스트를 닫을 , 남겨진 엔티티는 준영속 상태가 된다.

실제로 사용 보통 DB에서 데이터를 조회하고 트랜잭션이 종료되고 나서 반환 받은 엔티티가 준영속 상태이다.

 

삭제

삭제를 해도 바로 제거되진 않는다. flush 할때 DB에서 삭제된다.

 

 

영속성 컨텍스트 특징

영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태에선 식별자 값이 반드시 있어야 한다.

 

영속성 컨텍스트에 저장된 엔티티는 보통 트랜잭션이 커밋될 DB 저장된다.

엔티티를 DB 반영하는 것을 flush 한다.

 

영속성 컨텍스트 장점

1 캐시, 동일성(==) 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩

 

조회

 

https://medium.com/@seonggil/hibernate-second-level-cache-with-ehcache-5d27d6bec387

 

 

 

 

 

 

번째 조회했을 , 1 캐시에 존재하지 않아 DB에서 조회했다. 조회된 결과는 영속영 컨텍스트에 먼저 저장된 반환된다.

 

번째 조회했을 , 2 캐시에 존재해 DB에서 조회할 필요가 없다.

 

객체의 동일성 비교를 통해 같은 인스턴스인 것을 확일할 있다.

 

저장

명확히 구분되는 결과를 위해 자동 생성 KEY 제외했다.

이유는 영속성 컨텍스트는 식별자로 엔티티를 구분하는데, 이를 바꿔 말하면 반드시 식별자가 존재해야 영속성 컨텍스트에 저장할 있다는 뜻이다.

자동 생성 전략이 시퀀스인 경우

DB 먼저 접근해서 시퀀스를 받아온 값을 식별자로 엔티티를 영속화한다. 이후 쓰기 지연을 수행한다.

 



        private static void createPersistenceContext(Consumer<EntityManager> consumer) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        consumer.accept(em);
                        logger.info("###################### 커밋 ##########################");
                        tx.commit();
                } catch (Exception e) {
                        tx.rollback();
                } finally {
                        em.close();
                }
        }
        
        private static void save() {
                createPersistenceContext(em -> {
                        Member member = new Member();
                        member.setId(1L);
                        member.setName("홍길동");
                        em.persist(member);
                        
                        member = new Member();
                        member.setId(2L);
                        member.setName("임꺽정");
                        em.persist(member);
                        
                        member = new Member();
                        member.setId(3L);
                        member.setName("우투리");
                        em.persist(member);
                        
                });
        }
        

 

 

 

 

 

persist()호출을 하면, DB 바로 저장하는 것이 아니라 작업 큐에 SQL 저장해둔다.

그리고 영속성 컨텍스트에 저장한다.

이후 트랜잭션 커밋 DB 동기화 된다.

쓰기 지연은 물리적 데이터베이스 트랜잭션이 짧아져 데이터베이스의 잠금 경합이 줄어드는 효과 있다.

수정 (변경 감지)

SQL 직접 사용 문제점

일반적으로 직접 SQL Update 문을 작성할 비즈니스 요구사항이 변경되 추가되는 컬럼이 생기면, 따로 Update 문을 만들거나 기존 Update 문을 변경해야 한다.

SQL 직간접적으로 의존하게 된다.

 

JPA 수정

엔티티를 조회 값을 변경하면 알아서 DB 반영이 된다. 이를 변경 감지라 한다.

 



        private static void update() {
                createPersistenceContext(em -> {
                        Member m = em.find(Member.class, 1L);
                        m.setName("아무개");
                });
        }

 

참고용

수정 변경 감지

 

엔티티를 최초 영속성 컨텍스트에 저장할 상태를 스냅샷으로 저장을 한다.

그리고 플러시 시점에서 스냅샷을 기반으로 달라진 엔티티를 찾아 업데이트를 한다.

 

변경 감지는 이처럼 영속성 컨텍스트가 관리하는 상태에만 적용된다.

 



        private static void update() {
                createPersistenceContext(em -> {
                        Member m = em.find(Member.class, 1L);
                        m.setName("아무개");
                        em.detach(m); // 영속성 컨텍스트에서 분리하면, 업데이트 반영 안됨
                });
        }

 

JPA 수정 알아둘

하나의 변경이라도, 모든 필드를 수정한다.



@Entity
public class Member {
        @Id
//        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "MEMBER_ID")
        private Long id;
        private String name;
        //주의사항을 알기 위해 일부러 필드 추가
        private String dummy1;
        private String dummy2;
        private String dummy3;
        private String dummy4;
        

 

 

DB 바인드 변수를 제외한 모든 쿼리가 같으면, 쿼리를 재사용(캐시)한다.

필드가 많거나 수정할 너무 많은 데이터를 소비하게 된다면, 수정된 데이터만 수정하도록 하는 전략을 사용하면 된다.



@Entity
@DynamicUpdate
public class Member {
        @Id
//        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "MEMBER_ID")
        private Long id;
        private String name;
        //주의사항을 알기 위해 일부러 필드 추가
        private String dummy1;
        private String dummy2;
        private String dummy3;
        private String dummy4;
        

 

 

일반적으로 30 이하면, 정적 수정 쿼리가 빠르다.

참고로 테이블 컬럼이 30 이상인 경우는 테이블 설계가 잘못되었을 가능 성이 높다.

보통 정적으로 사용하다, 성능 이슈가 나오면 동적으로 사용하면 된다.

 

삭제

Member m = em.find(Member.class, 1L);
em.remove(m);

삭제 또한 remove()호출 즉시 제거되는 아니라 쓰기 지연 SQL 저장소에 등록 플러시 시점에 제거 SQL 수행된다.

, remove() 호출 이후로는 영속성 상태가 아니라는 것을 명심해야 한다.

 

 

 

플러시

플러시는 영속성 컨텍스트의 변경 내용을 DB 반영한다.

버퍼나 캐시에서 플러시 호출은 비우는 것이지만,

JPA에선 플러시가 비운다는 뜻이 아니다. 영속성 컨텍스트의 변경 내용을 DB 동기화 시키는 작업이다.

 

 

변경 감지가 동작해 Update 엔티티를 찾아 쓰기 지연 SQL 저장한다.

쓰기 지연 SQL 저장소에 담긴 CUD 쿼리를 DB 전송한다.

 

플러시 하는 방법 3 가지

  • flush() 직접 호출
  • 트랜잭션 커밋 자동 호출
  • JPQL 쿼리 실행 자동 호출

 



Member member = new Member();
member.setId(1L);
member.setName("홍길동");
em.persist(member);


member = new Member();
member.setId(2L);
member.setName("임꺽정");
em.persist(member);


TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
query.getResultList();


member = new Member();
member.setId(3L);
member.setName("우투리");
em.persist(member);

JPQL 경우 영속성 컨텍스트를 거치고 직접 DB 질의를 한다.

이때 영속성 컨텍스트의 엔티티의 변경 사항은 아직 DB 반영되지 않은 상태다.

따라서 JPQL 실행 전에 자동으로 플러시를 호출한다.

 

 

 

 

 

플러시 모드

기본은 AUTO

COMMIT 그대로 커밋할 때만 호출된다. JPQL 해당 없다.

 

준영속

영속성 상태에서 분리된 상태

영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.

 

크게 3가지

  • detach(엔티티) 호출
  • clear() 영속성 컨텍스트 초기화
  • close() 영속성 컨텍스트 종료

 

detach()



Member m1 = new Member();
m1.setId(1L);
m1.setName("홍길동");
em.persist(m1);


Member m2 = new Member();
m2.setId(2L);
m2.setName("임꺽정");
em.persist(m2);


Member m3 = new Member();
m3.setId(3L);
m3.setName("우투리");
em.persist(m3);


em.detach(m1);
em.detach(m2);
em.detach(m3);

 

 

영속성 컨텍스트에 저장된 엔티티들이 detach() 준영속이 되면서

1 캐시에서 제거, 쓰기 지연 SQL 저장소에서 제거 엔티티 관련된 모든 정보가 제거 된다.

 

clear()



Member m1 = new Member();
m1.setId(1L);
m1.setName("홍길동");
em.persist(m1);


Member m2 = new Member();
m2.setId(2L);
m2.setName("임꺽정");
em.persist(m2);


Member m3 = new Member();
m3.setId(3L);
m3.setName("우투리");
em.persist(m3);


em.clear();

플러시가 호출되기 전에 영속성 컨텍스트를 초기화했으므로 DB에는 아무것도 반영이 안된다.

 

close()



Member m1 = new Member();
m1.setId(1L);
m1.setName("홍길동");
em.persist(m1);


Member m2 = new Member();
m2.setId(2L);
m2.setName("임꺽정");
em.persist(m2);


Member m3 = new Member();
m3.setId(3L);
m3.setName("우투리");
em.persist(m3);


em.close();

일단 플러시가 되서 영속성 컨텍스트 변경사항은 DB 반영된다.

이후 영속성 컨텍스트가 종료되며 나머지 엔티티는 일반 자바 객체가 된다.

 

준영속 상태 특징

  • 거의 비용속 상태에 가깝다.
  • 식별자 값을 가지고 있다.
  • 지연 로딩을 없다.

 

 

merge() 병합

준영속 상태 엔티티를 다시 영속 상태로 변경할 사용한다.

Member m1 = new Member();
m1.setId(1L);
m1.setName("홍길동");
em.persist(m1);


Member m2 = new Member();
m2.setId(2L);
m2.setName("임꺽정");
em.persist(m2);


Member m3 = new Member();
m3.setId(3L);
m3.setName("우투리");
em.persist(m3);


em.flush(); // DB 동기화
em.clear(); // 영속성 컨텍스트 비우기 (모든 엔티티는 준영속 상태)


//준영속 상태 엔티티를 줘서 새로운 영속 상태 엔티티를 받는다.
m1 = em.merge(m1);
//merge()호출 전에 값을 바꿔도 상관 없다. flush() 시 바뀐 값으로 DB에 반영된다.
m3.setName("우투리2");
//주의, 반환 객체를 반드시 사용해야 한다.
em.merge(m2);
m3 = em.merge(m3);


m1.setName("홍길동2");
m2.setName("임꺽정2");


System.out.println("m1 " + (em.contains(m1) ? "관리 중" : "모름"));
System.out.println("m2 " + (em.contains(m2) ? "관리 중" : "모름"));
System.out.println("m3 " + (em.contains(m3) ? "관리 중" : "모름"));

 

 

머지를 호출해도 반환 받은 엔티티를 사용해야지 그대로 준영속 엔티티를 사용하면 안된다. 예제에서 m2 해당한다.

 

과정

  1. 먼저 1 캐시에서 찾는다.
  2. 없으면 DB에서 찾는다. 그리고 1 캐시에 저장한다.
    준영속 엔티티는 반드시 식별자 값을 가진다. 이전에 영속화 한적이 있기 때문

  1. DB 있었으므로, 변경 사항을 반영한다.(수정, 삭제)

식별자 값으로 DB에서 조회를 하기 때문에 merge() 전에 변경사항도 DB 반영된다.

 

 

비영속 병합

Member m1 = new Member();
m1.setId(1L);
m1.setName("홍길동");


Member m2 = new Member();
m2.setId(2L);
m2.setName("임꺽정");


Member m3 = new Member();
m3.setId(3L);
m3.setName("우투리");


m1 = em.merge(m1);
m2 = em.merge(m2);
em.merge(m3);
m2.setName("변경됨");
m3.setName("실수");

 

 

준영속일 때과 과정은 같다.

  1. 1 캐시에서 찾는다.
  2. 없으면 DB에서 찾는다. 그리고 1 캐시에 저장한다.

  1. DB에서 조회가 안되면, INSERT 한다.

조회해서 없으면 INSERT , 존재하면 UPDATE(혹은 DELETE) 이다.

 

merge() 반드시 반환된 엔티티를 사용해야 한다.

 

 

정리

영속성 컨텍스트는 애플리케이션과 DB 사이에서 가상의 DB 역할을 한다.

사이에서 1 캐시, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능을 제공한다.

 

영속성 컨텍스트에 저장된 엔티티는 플러시를 해야 DB 반영된다.

따라서 자동으로 플러시를 호출해주는 지점을 파악해야 한다.

일반적으로 트랜잭션

 

영속 상태 였다가 해제된 상태를 준영속이라 한다.

영속 상태에서 기능을 사용하지 못한다.

 

'개발 > JPA' 카테고리의 다른 글

06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22
04 엔티티 매핑  (1) 2024.02.26
02 JPA 소개  (0) 2023.12.01
01 JPA 환경 설정  (0) 2023.11.23

+ Recent posts