JPA (Java Persistence API)
- 자바 ORM 기술 표준
- 표준 명세
- JSR 338 - Java Persistence 2.2
JPA (Jakarta Persistence API)
- Jakarta Persistence 3.1
JPA 주요 Spec 및 Hibernate version
Java Persistence 2.2 (Hibernate 5.3+)
- Stream query results
- @Repeatable annotations
- Support Java 8 Date and Time API
- Support CDI Injection in AttributeConverters
Jakarta Persistence 3.1 (Hibernate 6.1+)
- UUID as Basic Java Type
- JPQL / Criteria API의 확장 - 날짜, 숫자 함수 추가 등
- ...
Hibernate 최신 버전
- Hibernate 5.6
- Hibernate 6.1
springboot 2.7.9 -> hibernate 5.6
JPA는 스펙; hibernate는 구현
SQL 설정
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=debug
binding parameters :: request ?에 대한 바인딩 로그
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
- cf.) org.hibernate.type.descriptor.sql.BasicExtractor :: result set 보여줌
기본 키 맵핑 전략
@GeneratedValue(strategy = GenerationType.IDENTITY)
자동 생성
- TABLE 전략: 채번 테이블을 사용
- SEQUENCE 전략: 데이터베이스 시퀀스를 사용해서 기본 키를 할당
- ex.) Oracle
- IDENTITY 전략: 기본 키 생성을 데이터베이스에 위임
- ex.) MySQL
- AUTO 전략: 선택한 데이터베이스 방언(dialect)에 따라 기본 키 맵핑 전략을 자동으로 선택
직접 할당
- 애플리케이션에서 직접 식별자 값을 할당
복합 Key (Composite key)
- @IdClass
- @EmbeddedId / @Embeddable
복합 Key Class 제약조건
- public class
- public 기본(no-arg) 생성자
- Serializable 인터페이스 구현
- equals(), hashCode() 메서드 정의
영속성 전이 (cascade)
바뀔 때 같이 바뀔래? 삭제/수정같이?
- Entity의 영속성 상태 변화를 연관된 Entity에도 함께 적용
- 연관관계의 다중성 (Multiplicity) 지정 시 cascade 속성으로 설정
@OneToOne(cascade = CascadeType.PERSIST)
@OneToMany(cascade = CascadeType.ALL)
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
cascade 종류
안 쓰면 각각 저장; 연관관계도 같이 저장
-> 멤버 저장하면 멤버 디테일도 저장
근데 두 엔티티의 lifecycle이 항상 같을 수 없어,, 진짜 필요할 때 고려해서 넣는 게 좋음
public enum CascadeType {
ALL, /* PERSIST, MERGE, REMOVE, REFRESH, DETACH */
PERSIST, // cf.) EntityManager.persist()
MERGE, // cf.) EntityManager.merge()
REMOVE, // cf.) EntityManager.remove()
REFRESH, // cf.) EntityManager.refresh()
DETACH // cf.) EntityManager.detach()
}
연관관계의 방향성
- 단방향(unidirectional)
- 양방향(bidirectional)
양방향 연관 관계
- 관계의 주인(owner)
- 양방향의 연관 관계의 주인은 외래 키(FK)가 있는 곳
- 주인만 @JoinColumn 어노테이션 사용 가능
- 연관 관계의 주인이 아닌 경우, mappedBy 속성으로 연관 관계의 주인을 지정
- 양방향의 연관 관계의 주인은 외래 키(FK)가 있는 곳
단방향 vs 양방향
단방향 맵핑만으로 연관관계 맵핑은 이미 완료
- JPA 연관관계도 내부적으로 FK 참조를 기반으로 구현하므로 본질적으로 참조의 방향은 단방향
단방향에 비해 양방향은 복잡하고 양방향 연관관계를 맵핑하려면 객체에서 양쪽 방향을 모두 관리해야 함
- 물리적으로 존재하지 않는 연관관계를 처리하기 위해 mappedBy 속성을 통해 관계의 주인을 정해야 함
단방향을 양방향으로 만들면 반대 방향으로의 객체 그래프 탐색 가능
- 우선적으로는 단방향 맵핑을 사용하고 반대 방향으로의 객체 그래프 탐색 기능이 필요할 때 양방향을 사용
- 일반적으로 단방향으로도 충분하지만 그게 아닌 경우가 있다.
- 일대다 연관관계 시 '다'에 해당하는 양만큼 update문이 나갈 수 있음 -> 양방향 연관관계 필요
- 복합키까지 쓰는 경우라면, 그리고 그 값이 FK에도 쓰는 경우라면 @MapsId로 지정해야 한다.
- 예시: 단방향으로 설정 시 원치 않은 update문이 나갈 수 있음
class Member{
...
@OneToMany(cascade = CascadeType.ALL) //member 바뀌면 아래도 알아서 바뀌라
@JoinColumn(name = "member_id")
private List<MemberDetail> details = new ArrayList<>();
}
//CASCADE 넣으면 이거 하나로 끝
memberRepository.save(member);
Hibernate: insert into members (create_dt, name, member_id) values (?, ?, ?)
Hibernate: insert into member_details (description, type, member_detail_id) values (?, ?, ?)
Hibernate: insert into member_details (description, type, member_detail_id) values (?, ?, ?)
Hibernate: update member_details set member_id=? where member_detail_id=?
Hibernate: update member_details set member_id=? where member_detail_id=?
insert 할 때 한 번에 하면 되지 않나 왜 update를?
해결: 양방향 일대다(1:N)로 변경
class Member{
...
@OneToMany(cascade = CascadeType.ALL, mappedBy = "member")
private List<MemberDetail> details = new ArrayList<>();
}
//
class MemberDetail{
...
@ManyToOne
@JoinColumn(name = "member_id")//column이름; 양방향일 때 관계 주인은 fk를 가지고 있는 여기!
private Member member;
}
///
//양방향이기 때문에 양쪽으로 다 세팅해야 함
memberDetail1.setMember(member);
member.getDetails().add(memberDetail1);
Repeated column in mapping for entity: com.jpa.entity.MemberDetail column: member_id (should be mapped with insert="false" update="false")
근데 에러가 남 두둥
@Entity
@Table(name = "MemberDetails")
public class MemberDetail {
@EmbeddedId
private Pk pk;
private String description;
@ManyToOne
@JoinColumn(name = "member_id")///
private Member member;
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public static class Pk implements Serializable {
@Column(name = "member_id") ////
private Long memberId;
private String type;
}
}
@JoinColumn, @pk안의 @Column 모두 업데이트 가능하게 만들어서 에러
하나는 조회용이라고 밝혀줘야
@ManyToOne
@JoinColumn(name = "member_id", insertable = false, updatable = false)
private Member member;
그러면 이렇게 하면 될까? nope!
왜냐면 cascade 때문에. 그걸로 업데이트/인서트 하겠다고 한 건데 안 하겠다고 하면(updatable false) 안되지
그렇다면?
@ManyToOne
@MapsId("memberId")
private Member member;
pk에도 쓰이는 칼럼이 @JoinColumn에서도 써야 한다면, 같은 거를 쓴다고 알려줘야 함
MapsId 만 써도 되는데 PK가 복합키라서 그중에 뭐? 를 알려줘야 할 때
PK에서 사용되는 콜롬이 FK에도 쓰인다! @MapsId(변수명)
그러면 insert 세 개만 나간다!
정리: 양방향을 맺자
@Entity
@Table(name = "Orders")
public class Order {
...
@OneToMany(mappedBy = "order", cascade = CascadeType.Merge, CasecadeType.PERSIST)//order 저장 시 detail도 저장하게 하려면 여기다가 cascade option 필요
private List<OrderDetail> details = new ArrayList<>();
}
////
@Entity
@Table(name = "OrderDetails")
public class OrderDetail {
...
@EmbeddedId
private Pk pk = new Pk();
@ManyToOne
// @JoinColumn(name = "order_id") //원래대로라면 이렇게 하지만 PK에도 사용되니..
@MapsId("orderId")
private Order order;
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public static class Pk implements Serializable {
@Column(name = "order_id")
private Long orderId;
private String type;
}
}
////
@Transactional
public void doSomething() {
Order order = new Order();
order.setOrderDate(LocalDateTime.now());
OrderDetail orderDetail1 = new OrderDetail("type1");
orderDetail1.setDescription("order1-type1");
orderDetail1.setOrder(order);
OrderDetail orderDetail2 = new OrderDetail("type2");
orderDetail2.setDescription("order1-type2");
orderDetail2.setOrder(order);
order.getDetails().add(orderDetail1);
order.getDetails().add(orderDetail2);
orderRepository.save(order); ///여기서 한번만 해도 detail이 들어가려면 Cascade가 필요한 것이다!
}
N + 1 문제
JPA에서 N+1 문제는 자식 엔티티가 얼마나 있는지에 관계없이 부모 엔티티 개수에 따라 추가적인 쿼리가 발생하기 때문에, 자식 엔티티의 개수와는 상관이 없습니다. 이 문제는 정확히 부모 엔티티의 개수와 관련이 있습니다. 좀 더 명확히 설명하자면 다음과 같습니다:
1. N+1 문제의 본질
N+1 문제란, 부모 엔티티를 조회하는 1번의 쿼리와, 각 부모 엔티티에 대해 자식 엔티티를 조회하기 위한 N번의 추가 쿼리가 발생하는 문제를 의미합니다. 여기서 N은 부모 엔티티의 개수를 의미합니다.
- 1번의 쿼리: 부모 엔티티를 조회하는 쿼리입니다.
- N번의 쿼리: 각 부모 엔티티마다 자식 엔티티를 조회하는 쿼리가 발생하는 것입니다.
이 문제는 자식 엔티티의 수가 아니라, 부모 엔티티의 수만큼 추가적인 쿼리가 발생하는 것이 문제의 핵심입니다.
2. 부모 1건, 자식 여러 건의 경우
부모 엔티티가 1건이라면 자식 엔티티가 아무리 많더라도, 부모 엔티티를 조회한 후 자식 엔티티를 조회하기 위한 쿼리가 단 1번 발생합니다.
@Entity
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Child> children;
}
@Entity
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
- 쿼리 한 번으로 N 건의 레코드를 가져왔을 때, 연관관계 Entity를 가져오기 위해 쿼리를 N번 추가 수행하는 문제
@Entity
@Table(name = "Orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long orderId;
@ManyToOne(optional = false) //eager -> join
@JoinColumn(name = "customer_id")
private Customer customer;
@Column(name = "order_dt")
private LocalDateTime orderDate;
@OneToMany(cascade = CascadeType.ALL) //lazy 아직..
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems;
}
//
@Entity
@Table(name = "OrderItems")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_line_id")
private Long orderLineId;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
private Long quantity;
}
public void getOne() {
//단건은 연관관계 매핑을 고려해서 join사용
orderRepository.findById(1L);
}
public void getMulti() {
//findall은 명확하게 모르는 쿼리를 날리면 우선 실행하고 연관관계매핑을 적용
orderRepository.findAll();
}
단 건을 실행하면 join으로 한 번에 가져오는데
findAll실행하면 우선 order 실행하고 eager인 customer 실행
Hibernate:
select
order0_.order_id as order_id1_3_,
order0_.customer_id as customer3_3_,
order0_.order_dt as order_dt2_3_
from
orders order0_
//
select
customer0_.customer_id as customer1_0_0_,
customer0_.customer_name as customer2_0_0_
from
customers customer0_
where
customer0_.customer_id=?
//
select
customer0_.customer_id as customer1_0_0_,
customer0_.customer_name as customer2_0_0_
from
customers customer0_
where
customer0_.customer_id=?
feachType의 문제가 아니야!
해결 방법
- Fetch Join
- join을 사용하여 쿼리 하나로 실행하겠다.
- JPQL join fetch
- fetch 없으면, from절에 있는 메인 엔티티만 반환. 우선 진행시켜! 그 후에 from절 전에 있는 메인 엔티티를 하나씩 따져보면서 다시 N+1 실행
- fetch를 써야 select절에 다른 것들도 받음
- Querydsl fetchJoin()
- 그룹화하여 쿼리 실행 횟수를 줄이겠다.
- Entity Graph //선언적으로 어디까지 탐색할 수 있는지 지정가능
- 그 외
- Hibernate @BatchSize //나눠서
- Hibernate @Fetch(FetchMode.SUBSELECT) //in절에 넣어서 실행
- 주의. 그지 같은 join문으로 인해.. 성능이 더 나빠질 수 있음.
@Query("select o from Order o "
+ " inner join fetch o.customer as c "
+ " left join fetch o.orderItems as oi "
+ " inner join fetch oi.item as i")
List<Order> getOrdersWithAssociations();
Querydsl
복잡한 쿼리 작성 시 컴파일러의 도움을 받을 수 있음
- JPA에서 제공하는 객체 지향 쿼리
- JPQL: Entity 객체를 조회하는 객체 지향 쿼리 // text 기반이라 compiler의 도움을 못 받음
- Criteria API: JPQL을 생성하는 빌더 클래스 //복잡함
- third party library를 이용하는 방법
- Querydsl
- jOOQ //native query 기반
- …
JPQL vs Criteria API
- JPQL
- SQL을 추상화해서 특정 DBMS에 의존적이지 않은 객체지향 쿼리
- 문제 : 결국은 SQL이라는 점
- 문자 기반 쿼리이다 보니 컴파일 타임에 오류를 발견할 수 없다
- Criteria API
- 프로그래밍 코드로 JPQL을 작성할 수 있고 동적 쿼리 작성이 쉽다
- 컴파일 타임에 오류를 발견할 수 있고 IDE의 도움을 받을 수 있다
- 문제 : 너무 복잡
Querydsl
- Criteria API처럼 정적 타입을 이용해서 JPQL을 코드로 작성할 수 있도록 해 주는 오픈소스 프레임워크
- Criteria API에 비해 복잡하지 않고 매우 편리하고 실용적
Spring Data JPA + Querydsl
- QuerydslPredicateExecutor
- QuerydslRepositorySupport //join이 많을 경우; 추상 interface -> impl
Custom Repository 구현
Custom Repository를 위한 interface 생성
@NoRepositoryBean
public interface MemberRepositoryCustom {
List<Member> getMembersWithAssociation();
}
Custom Repository 기능 구현
- QuerydslRepositorySupport 클래스 상속
- 기본 생성자에서 Entity를 인자로 전달받는 상위 클래스의 생성자를 호출
- Custom Repository interfade 구현
- 구현 메서드에서 Querydsl의 Q-type class를 이용해서 쿼리 수행
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
public MemberRepositoryImpl() {
super(Member.class);
}
@Override
public List<Member> getMembersWithAssociation() {
// ...
}
}
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<configuration>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/annotations/java</outputDirectory> //해당 경로에 만든다
</configuration>
</execution>
</executions>
</plugin>
기본 Repository interface 변경
- 기본 Repository interface가 Custom Repository interface를 상속받도록
public interface MemberRepository extends MemberRepositoryCustom, JpaRepository<Member, Long> {
}
기본 Repository interface를 이용해서 Custom Repository interface에 선언된 확장 메서드 호출
트러블 슈팅 - Custom Repository 구현 시 흔히 하는 실수: naming rule
- 기본 Repository interface
- MemberRepository
- Custom Repository interface
- MemberRepositoryCustom
- Custom Repository 구현 class
- MemberRepositoryCustomImpl (X)
- MemberRepositoryImpl (O)
참고) query dsl 사용 예시
@Override
public List<Student> getStudentsWithAssociations() {
QStudent student = QStudent.student;//1개만 쓰면
QStudent student1 = new QStudent("student1");//inner query 등으로 추가적으로 더 필요하면 인스턴스 더 만들어
QEnrollment enrollment = QEnrollment.enrollment;
QCourse course = QCourse.course;
return from(student)
.leftJoin(student.enrollments, enrollment).fetchJoin() //연관관계, q타입 걸치고
.innerJoin(enrollment.course, course).fetchJoin()
// .where(student.name.eq("gg"))
// .select(student.studentId)
// .fetchOne()
.fetch() //list 반환 시 fetch; 하나만 가져올거면 fetchOne
;
}
Repository 설정
@EnableJpaRepositories
public @interface EnableJpaRepositories {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
String repositoryImplementationPostfix() default "Impl";
// ...
}
Spring Boot에서는
dependency에 spring-data-jpa를 추가하면 @EnableJpaRepositories 없어도 기본으로 세팅해 줌
- Spring Data JPA Repository를 위한 auto configuration
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
- @Import(JpaRepositoriesImportSelector)
- JpaRepositoriesRegistrar
- @EnableJpaRepositories
- JpaRepositoriesRegistrar
- @Import(JpaRepositoriesImportSelector)
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
Spring Data Repository 메서드 탐색 순서
- Spring Data Repository 추상화 덕분에 interface 선언만으로도 쿼리 생성 가능
- interface에 선언된 메서드의 실제 구현체는 아래 순서로 탐색하게 된다
- 기본 구현체(JpaRepository의 구현체인 SimpleJpaRepository::saveAll etc.)
- 메서드 이름 규칙 적용 (cf. 메서드 이름으로 쿼리 생성 findByNameLikeAndPhone etc.)
- Custom Repository Implementation
- MemberRepositoryImpl -> MemberRepositoryCustomImpl 로 바꾸면? O
- OrderRepositoryImpl -> OrderRepositoryCustomImpl 로 바꾸면? O
Repository Fragment 를 이용한 Custom Repository 구현
: 최근에 바뀜; 하나의 커스텀 레파지토리에 다 넣을 필요 없이 여러개로 구현체를 나눠서 구현 가능(repository fragment)
: 나눠서 구현하고 주 repository에 상속하듯 사용해도 된다
Repository Fragment 를 이용한 Custom Repository
// no `@NoRepositoryBean`
public interface CustomizedMemberRepository {
List<Member> getMembersWithAssociation();
}
// NOT MemberRepository + `Impl`
// BUT **CustomizedMemberRepository** + `Impl`
public class CustomizedMemberRepositoryImpl implements CustomizedMemberRepository {
// ...
}
여러 개의 Custom Repository 구현 가능
- 앞서 본 CustomizedMemberRepository interface와 CustomizedMemberRepositoryImpl class 와 같이
- 예를 들면 GuestRepository interface, GuestRepositoryImpl class 같이 여러 개의 Custom Repository 구현 가능
Custom Repository 들로 구성된 Repository
public interface MemberRepository
extends CustomizedMemberRepository, GuestRepository {
}
참고) @Repository 어노테이션이란(not jpa 과거에..)
- 방식: streotype bean -> 해당 이름으로 된 빈들을 찾아서 자동으로 등록
그러나 jpa는.. 그 방식이 아니고
- interface extends JpaRepository -> 다 뒤져서 jpa 후보군으로 등록
- @NoRepositoryBean -> 그 후보군에서 빼줘
Qtype이 안 생겨서 -> no complie... -> 못 찾으면.. 세상 망함.. 온 세상이 빨개요..
cf.) 트러블 슈팅 - Querydsl Q-type class variable로 "member"나 "order"를 쓸 수 없다?!
- Querydsl에서 Q-type class 인스턴스 생성 시 variable을 "member"로 주면 에러 발생
예제
QMember member = new QMember("member");
QOrder order = new QOrder("order");
unexpected token: member
unexpected token: order
이유
- JPQL에 MEMBER OF 연산자가 있기 때문에 MEMBER가 예약어라 variable에 쓸 수 없음
@Query("SELECT m FROM Member m WHERE :detail MEMBER OF m.details")
Member getMemberContainingDetail(MemberDetail detail);
- 마찬가지로 JPQL에 ORDER BY 연산자가 있기 때문에 ORDER가 예약어라 variable에 쓸 수 없음
해결방법
- 아래 코드에서 각각의 Q-type class의 static 변수는 variable 값이 뭐라고 되어 있을까?
QMember member = QMember.member;
QOrder order = QOrder.order;
member, order :: 예약어; 다른걸로 쓰면 된다.
소스를 까보면 지들도 member를 회피하기 위해 member1로 쓰고있음
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {
private static final long serialVersionUID = 1029385075L;
public static final QMember member = new QMember("member1"); /// member를 회피하기 위한
'개발 > spring' 카테고리의 다른 글
[문제해결] Execution failed for task ':test'. > No tests found for given includes: (0) | 2023.03.23 |
---|---|
[spring-jpa] N+1 문제 관련 jpa 돌아보기 -2 (0) | 2023.03.15 |
[spring framework 6, boot 3] http rest clients (0) | 2023.03.11 |
[springboottest] 테스트에서 h2-console 사용하기 (0) | 2023.02.23 |
ModelAttribute vs RequestBody data bind, 직렬화 & 역직렬화 (0) | 2023.02.21 |