QueryDsl 왜 쓰나요?

JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다.
간단한 로직은 큰 문제가 없으나, 복잡한 로직의 경우 쿼리 문자열이 상당히 길어집니다.
JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생하게 됩니다.

이러한 문제를 어느정도 해소하는데 기여하는 프레임워크가 바로 QueryDsl 입니다.
QueryDsl은 정적 타입을 이용해서 Query를 생성해주는 프레임워크입니다.


QueryDsl의 장점과 단점은 무엇인가요?


QueryDsl 장점

1) 문자가 아닌 코드로 쿼리를 작성함으로써 컴파일 시점에 문법 오류를 쉽게 확인할 수 있습니다.

2) IDE의 자동완성으로 도움을 받을 수 있습니다.

3) 동적인 쿼리 작성이 편리합니다.

4) 쿼리 작성시 제약 조건 등을 메서드 추출을 통해 간편하고 재사용할 수 있습니다.


QueryDsl 단점

1) 번거로운 Gradle 설정 및 사용법을 배워야 합니다.



QueryDsl 예제 코드

  • Springboot 2.7.10, java 11, gradle 8.4 기반으로 작성한 예제 코드입니다.
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.14'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.oauth2.resource.server'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '11'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.14'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    // queryDsl 설정
    implementation "com.querydsl:querydsl-jpa"
    implementation "com.querydsl:querydsl-core"
    implementation "com.querydsl:querydsl-collections"
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
    delete file(generated)
}

//tasks.named('test') {
//    useJUnitPlatform()
//}
사용자 Entity
package com.cafe.be.api.common.entity;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.sql.Timestamp;
import java.time.LocalDateTime;

@MappedSuperclass
public class BaseEntity {

    // 생성일
    @CreationTimestamp
    private LocalDateTime createDt;

    // 수정일
    @UpdateTimestamp
    private LocalDateTime modifyDt;

}

package com.cafe.be.api.userservice.entity;

import com.cafe.be.api.common.entity.BaseEntity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Users extends BaseEntity {

    // 사용자 번호
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userNo;

    // 이메일
    private String email;

    // 비밀번호
    private String pw;

    // 닉네임
    private String nick;

    // 사용자 상태
    private String status;

    // 권한
    private String role;

    // 생년월일
    private String birth;

    // 이용약관 동의
    private String agree1;

    // 개인정보 수집 및 이용 동의
    private String agree2;

    // 이벤트, 프로모션, 메일, sms 수신
    private String agree3;

}
사용자 Repository
package com.cafe.be.api.userservice.repo;

import com.cafe.be.api.userservice.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UsersRepository extends JpaRepository<Users, Long> {
}
QueryDsl Config
package com.cafe.be.api.common.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig{

    private final EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(em);
    }
}
사용자 QueryDsl
package com.cafe.be.api.userservice.querydsl;

import com.cafe.be.api.userservice.dto.QUsersResponse;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import static com.cafe.be.api.userservice.entity.QUsers.users;

@Repository
@RequiredArgsConstructor
public class UserQueryDsl {

    private final JPAQueryFactory queryFactory;

    public UsersResponse findByUserNo(Long userNo){
        UsersResponse usersResponse = queryFactory
                .select(
                        new QUsersResponse(
                                users.userNo,
                                users.email,
                                users.pw,
                                users.nick,
                                users.status,
                                users.role,
                                users.birth,
                                users.agree1,
                                users.agree2,
                                users.agree3
                        )
                ).from(users)
                .where(
                        users.userNo.eq(userNo)
                ).fetchFirst();

        return usersResponse;
    }

}
사용자 DTO
package com.cafe.be.api.userservice.dto;

import com.cafe.be.api.userservice.entity.Users;
import com.querydsl.core.annotations.QueryProjection;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@Builder
public class UsersResponse {

    private Long userNo;

    // 이메일
    private String email;

    // 비밀번호
    private String pw;

    // 닉네임
    private String nick;

    // 사용자 상태
    private String status;

    // 권한
    private String role;

    // 생년월일
    private String birth;

    // 이용약관 동의
    private String agree1;

    // 개인정보 수집 및 이용 동의
    private String agree2;

    // 이벤트, 프로모션, 메일, sms 수신
    private String agree3;

    @QueryProjection
    public UsersResponse(Long userNo, String email, String pw, String nick, String status, String role, String birth, String agree1, String agree2, String agree3) {
        this.userNo = userNo;
        this.email = email;
        this.pw = pw;
        this.nick = nick;
        this.status = status;
        this.role = role;
        this.birth = birth;
        this.agree1 = agree1;
        this.agree2 = agree2;
        this.agree3 = agree3;
    }


    public static UsersResponse UsersToUsersResponse(Users u){
        UsersResponse usersResponse = UsersResponse.builder()
                .userNo(u.getUserNo())
                .email(u.getEmail())
                .pw(u.getPw())
                .nick(u.getNick())
                .status(u.getStatus())
                .role(u.getRole())
                .birth(u.getBirth())
                .agree1(u.getAgree1())
                .agree2(u.getAgree2())
                .agree3(u.getAgree3())
                .build();

        return usersResponse;

    }

}
사용자 Controller
package com.cafe.be.api.userservice.controller;

import com.cafe.be.api.common.dto.CommonResponse;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.cafe.be.api.userservice.entity.Users;
import com.cafe.be.api.userservice.repo.UsersRepository;
import com.cafe.be.api.userservice.service.UsersService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UsersController {

    private final UsersService usersService;

    @GetMapping("/{userNo}")
    public CommonResponse<UsersResponse> detail(@PathVariable("userNo") Long userNo){

        return usersService.detail(userNo);

    }

}
사용자 Service
package com.cafe.be.api.userservice.service;

import com.cafe.be.api.common.dto.CommonResponse;
import com.cafe.be.api.common.enums.ResponseCode;
import com.cafe.be.api.common.exception.ApiException;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.cafe.be.api.userservice.entity.Users;
import com.cafe.be.api.userservice.querydsl.UserQueryDsl;
import com.cafe.be.api.userservice.repo.UsersRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class UsersService {

    private final UsersRepository usersRepository;

    private final UserQueryDsl userQueryDsl;

    public CommonResponse<UsersResponse> detail(Long userNo) {

        log.debug("[UsersService] [detail] request >>> {}", userNo);

        CommonResponse<UsersResponse> result = new CommonResponse<>();

        try{
//            Users users = usersRepository.findById(userNo)
//                    .orElseThrow(() -> new ApiException(ResponseCode.USER_NOT_FOUND));
//
//            UsersResponse usersResponse = UsersResponse.UsersToUsersResponse(users);


            UsersResponse usersResponse = userQueryDsl.findByUserNo(userNo);

            result.setResult(usersResponse);
            result.setCode(ResponseCode.SUCCESS.getCode());
            result.setMsg(ResponseCode.SUCCESS.getMsg());

        }catch (ApiException e){
            result.setCode(e.getCode());
            result.setMsg(e.getMsg());
        }

        return result;
    }
}
반응형

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

JPA-OSIV(Open Session In View)  (0) 2023.10.14
[JPA] could not initialize proxy - no Session  (0) 2023.09.10
[JPA] GeneratedValue  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10

OSIV(Open Session In View)

  • OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어두는 기능이다.
    영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지된다.
    뷰까지 영속성 컨텍스트가 살아있다면 뷰에서도 지연 로딩을 사용할 수 가 있다.

  • JPA에서는 OEIV(Open EntityManager In View), 하이버네이트에선 OSIV(Open Session In View)라고 한다.

OSIV 동작 원리

  • Spring Framework가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV다.
    영속성 컨텍스트는 사용자의 요청 시점에서 생성이 되지만, 데이터를 쓰거나 수정할 수 있는 트랜잭션은 비즈니스 계층에서만 사용할 수 있도록 트랜잭션이 일어난다.

업로드중..

  • spring.jpa.open-in-view: true 기본값

  • Spring Boot JPA 의존성을 주입 받아 어플리케이션을 구성할 경우 spring.jpa.open-in-view의 기본값인 true로 지정되어 있어 OSIV가 적용된 상태로 어플리케이션이 구성된다.

동작원리

  • 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단 이 시점에서 트랜잭션은 시작하지 않는다.
  • 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
  • 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이 시점에 트랜잭션은 끝내지만 영속성 컨텍스트는 종료되지 않는다.
  • 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
  • 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.

서블릿 계층에서 트랜잭션이 끝나면 컨트롤러와 뷰에는 트랜잭션이 유지되지 않는 상태이다.
엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 동작하는데, 이것을 트랜잭션 없이 읽기(Nontransactional reads)라 한다. 하여 만약 프록시를 뷰 렌더링하는 과정에 초기화(Lazy loading)가 일어나게 되어도 조회 기능이므로 트랜잭션이 없이 읽기가 가능하다.

  • 영속성 컨텍스트는 기본적으로 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
  • 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. 이것을 트랜잭션 없이 읽기(Nontransactional reads)라 한다.

만약 트랜잭션 범위 밖인 컨트롤러와 뷰에서 엔티티를 수정하여도 영속성 컨텍스트의 변경 감지에 의한 데이터 수정이 다음 2가지 이유로 동작하지 않는다.

  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면 영속성 컨텍스트를 플러시(flush)해야 한다. 스프링이 제공하는 OSIV는 요청이 끝나면 플러시를 호출하지 않고 em.close()로 영속성 컨텍스트만 종료시켜 버린다.
  • 프레젠테이션 계층에서 em.flush()를 호출하여 강제로 플러시해도 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외가 일어난다.(javax.persistence.TransactionRequiredException)

OSIV 사용시 주의점

  • OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다.
    그래서 View Template이나 API 컨트롤러에서 지연 로딩이 가능하다.

  • 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다.

  • OSIV의 치명적인 단점: 커넥션을 영속성 컨텍스트가 종료될 때까지 1:1로 계속 물고 있다.

OSIV false

업로드중..

  • spring.jpa.open-in-view: false (OSIV 종료)
  • OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다.
  • OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다.
    결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.

커맨드와 쿼리 분리

실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다. 바로 Command와 Query를 분리하는 것이다.

보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다. 그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다. 하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아니다.
그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미가 있다.
단순하게 설명해서 다음처럼 분리하는 것이다.

OrderService

  • OrderService: 핷심 비즈니스 로직

  • OrderQueryService: 화면이나 API에 맞춘 서비스(주로 읽기 전용 트랜잭션 사용)

  • 보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수 있다.

OSIV 정리

특징

  • OSIV는 클라이언트 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다. 하여 한 번 조회된 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다.
  • 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다. 트랜잭션이 없는 프레젠테이션 계층은 지연 로딩을 포함해 조회만 할 수 있다.

단점

  • 영속성 컨텍스트와 DB 커넥션은 1:1로 물고있는 관계이기 때문에 프레젠테이션 로직까지 DB 커넥션 자원을 낭비하게 된다.
  • OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유하게 될 수도 있다.
  • 프레젠테이션에서 엔티티를 수정하고 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다.
  • 프레젠테이션 계층에서 렌더링 과정에서 지연 로딩에 의해 SQL이 실행된다. 따라서 성능 튜닝시에 확인해야 할 부분이 넓어진다.
반응형

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

QueryDsl이란? 왜 사용하는가?  (0) 2023.12.15
[JPA] could not initialize proxy - no Session  (0) 2023.09.10
[JPA] GeneratedValue  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10
반응형

원인

Member와 Message가 연관관계를 맺고 있고 1:N 관계를 가지고 있다.
Message 엔티티에서 Member Fetch 전략을 Lazy로 설정해준 상태이다.

오류 예시

// 메시지 제목, 내용, 작성자 이름, 받는 사람 이름 등의 정보가 들어있는 MessageResponseDTO로 변환하여 리소스를 반환하는 API

@GetMapping("/message/{message_id}")
public Result readMessage(@PathVariable Long message_id){
    Message msg = messageService.findB yId(message_id);
    MessageResponseDto messageResponseDto = MessageResponseDto.covertMessageDto(msg);
    return responseService.getSingleResult(messageResponseDto);
}

오류발생

  • Message를 단건조회하면 Message와 Lazy Loding으로 연관된 Member는 바로 초기화 되지 않고 필요할 때 정보가 채워지는 프록시 객체로 채워진다.

Message = Message 필드 + Member Proxy 객체

  • Member의 값을 써서 DTO를 채워야하는데 Member의 값이 초기화 되지 않은 상태라서 DTO를 만들 수 없음.

Lazy Loding방식이니 변환하면서 데이터를 사용할 때 쿼리를 날려 Proxy 객체를 채우지 못함.
-> Service에서 트랜잭션이 일어나도록 설정을 하였음. JPA 영속성 컨텍스트는 보통 트랜잭션과 생명주기를 같이한다. 그 말은 Service -> Controller로 나오면서 영속성 상태가 끝난다는 뜻이다. 더이상 영속성 컨텍스트에서 관리하지 않고 Member에 필요한 값이 있을때 쿼리를 날려 Proxy 객체를 채우지 않는다는 뜻이다.

해결방법

  1. Message -> DTO 변환을 컨트롤러 단에서 서비스 단으로 변경
@GetMapping("/message/{message_id}")
public Result readMessage(@PathVariable Long message_id){
    MessageResponseDto messageResponseDto = messageService.findByMessageId(message_id);
    return responseService.getSingleResult(messageResponseDto);
}
  1. Message에 있는 Member를 즉시로딩(Eager)로 변경한다.
  • 추천하지 않음.
반응형

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

QueryDsl이란? 왜 사용하는가?  (0) 2023.12.15
JPA-OSIV(Open Session In View)  (0) 2023.10.14
[JPA] GeneratedValue  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10

주의점

Hibernate 에서는 여러개의 데이터를 한번에 Insert, Update 하게 해주는 기능인 Batch 기능을 지원하고 있다.

jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        generate_statistics: true
        dialect: org.hibernate.dialect.H2Dialect
        show_sql: true
        format_sql: true
        order_inserts: true
        order_updates: true
        jdbc:
          batch_size: 1000

Spring Data JPA의 saveAll 함수를 사용하면 여러개의 Insert 또는 Update 할 수 있다.

하지만, @GeneratedValue 키 값 생성 전략을 Identity나 Auto로 정하는 경우 Hibernate에서 Batch Insert 기능을 비활성화 시켜놓고 Insert 작업을 수행하게 된다.

따라서 saveAll 같은 함수를 사용해도 데이터 개수만큼 Insert 또는 Update 쿼리가 나간다.

반응형

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

JPA-OSIV(Open Session In View)  (0) 2023.10.14
[JPA] could not initialize proxy - no Session  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10
[Spring] NativeQuery  (0) 2023.09.10
반응형

JPA Auditing이란?

Java에서 ORM 기술인 JPA를 사용하여 도메인을 관계형 데이터베이스 테이블에 매핑할 때 공통적으로 도메인들이 가지고 있는 필드나 컬럼들이 존재합니다. 대표적으로 생성일자, 수정일자, 식별자 같은 필드 및 컬럼이 있습니다.
도메인마다 공통으로 존재한다는 의미는 결국 코드가 중복된다는 말과 일맥상통합니다.
데이터베이스에서 누가, 언제하였는지 기록을 잘 남겨놓아야 합니다. 그렇기 때문에 생성일, 수정일 컬럼은 대단히 중요한 데이터입니다.
그래서 JPA에서는 Audit 이라는 기능을 제공하고 있습니다. Audit은 감시하다, 감사하다라는 뜻으로 Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능입니다. 도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우 매번 시간 데이터를 입력하여 주어야 하는데, audit을 이용하면 자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 됩니다.

Auditing 활성화 하기

  • 가장 먼저 SpringBootApplication에 @EnableJpaAuditing 어노테이션을 추가해줍니다.
@EnableJpaAuditing
@SpringBootApplication
public class TestApplication{
    public static void main(String[] argAS){
        SpringApplication.run(TestApplication.class, args);
    }
}

BaseEntity 생성하기

  • Auditing이 필요한 Entity에서 상속받을 BaseEntity를 생성합니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity{
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime updatedDate;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String modifiedBy;
}

@MappedSuperclass (javax.persistence)

  • Entity에서 Table에 대한 공통 매핑 정보가 필요할 때 부모 클래스에 정의하고 상속받아 해당 필드를 사용하여 중복을 제거

@EntityListenrs (javax.persistence)

  • Entity를 DB에 적용하기 이전, 이후에 커스텀 콜백을 요청할 수 있는 어노테이션

Class AuditingEntityListner (org.springframework.data.jpa)

  • Entity 영속성 및 업데이트에 대한 Auditing 정보를 캡처하는 JPA Entity Listener

@CreatedDate (org.springframework.data)

  • 데이터 생성 날짜 자동 저장 어노테이션

@LastModifiedDate (org.springframework.data)

  • 데이터 수정 날짜 자동 저장 어노테이션

@CreatedBy (org.springframework.data)

  • 데이터 생성자 자동 저장 어노테이션

@LastModifiedBy (org.springframework.data)

  • 데이터 수정자 자동 저장 어노테이션

Entity에 적용하기

@Getter
@Entity
@NoArgsConstructor(access = PROTECTED)
public class class Users extends BaseEntity{
    @Id
    @GeneratedValue
    @Column(name = "user_id")
    private Long id;

    private String name;
}

@CreatedBy, @ModifiedBy 사용하기

org.springframework.data.domain.AuditorAware를 스프링 빈으로 등록해야 합니다.

public interface AuditorAware<T> {
    /**
    * Returns the current auditor of the application.
    *
    * @return the current auditor.
    */
    Optional<T> getCurrentAuditor();
}

AuditorAware 인터페이스는 Optional를 반환하는 method가 하나 있기 때문에 아래 코드처럼 람다로 AuditorAware를 구현한 객체를 반환할 수 있습니다.

@Bean
public AuditorAware<String> auditorProvider(){
 // 람다를 이용
  return () -> Optional.of(UUID.randomUUID().toString());

  // 익명 클래스를 이용
  return new AuditorAware<String>(){
      @Override
      public Optional<String> getCurrentAuditor(){
          return Optional.of(UUID.randomUUID().toString());
      }
  }
}
반응형

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

[JPA] could not initialize proxy - no Session  (0) 2023.09.10
[JPA] GeneratedValue  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10
[Spring] NativeQuery  (0) 2023.09.10
[JPA] Cascade  (1) 2023.09.10
반응형

@MappedSuperclass

  • 객체의 입장에서 공통 매핑 정보가 필요할 때 사용한다.
  • id, name, created_at, updated_at은 객체의 입장에서 볼 때 계속 나온다.
  • 이렇게 공통 매핑 정보가 필요할 때 부모 클래스에 선언하고 속성만 받아서 사용하고 싶을 때 @MappedSuperclass를 사용한다.테이블 구조
-- Member 테이블
create table member(
    id bigint,
    created_at date,
    updated_at date
);
-- Team 테이블
create table team(
    team_name varchar(128),
    created_at date,
    updated_at date
);

코드로 이해하기

  • 생성시간, 수정시간을 모든 엔티티에 공통으로 가져가야 하는 상황
  • BaseEntity.java
    • 매핑정보만 상속받는 Superclass라는 의미의 @MappedSuperclass 어노테이션 선언
@Getter
@Setter
@MappedSuperclass
public abstact class BaseEntity{
    private LocalDate createdAt;
    private LocalDate update_at;
}
@Entity
public class Member extends BaseEntity{
    @Id
    private Long id;
}
@Entity
public class Team extends BaseEntity{
    @Id
    private String teamName;
}

정리

  • 상속관계 매핑이 아니다.
  • @MappedSuperclass가 선언되어 있는 클래스는 엔티티가 아니다. 당연히 테이블과 매핑도 안된다.
  • 단순히 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다.
  • 부모 타입으로 조회, 검색이 불가능하다.
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것을 권장한다.
  • 주로 등록일, 수정일 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
  • JPA에서 @Entity 클래스는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속할 수 있다.
반응형

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

[JPA] GeneratedValue  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[Spring] NativeQuery  (0) 2023.09.10
[JPA] Cascade  (1) 2023.09.10
[JPA] @OneToMany orphanRemoval  (0) 2023.09.10
반응형

@Query

  • JPA에 정의된 키워드를 조합하면 특정조건에 해당하는 데이터를 원하는 형태대로 가지고 올 수 있습니다. 하지만 데이터베이스에 종속적인 문법을 사용해야 할 때나 Entity 간의 명시적으로 들어나지 않는 관계간의 조인, 데이타 조회 속도 향상등의 목적으로 직접 쿼리를 작성할 수 있는 방법을 제공하고 있습니다.
  • @Query 속성중에 nativeQuery 속성을 true로 설정하지 않았따면 기본적으로 JPQL 문법으로 동작이 됩니다.
  • JPQL 문법은 JPA 에서 사용되는 언어이며 쿼리 구문과 유사하나 Table이 아닌 Entity를 기준으로 데이터를 조회한다는 것이 다릅니다.

NativeQuery란?

  • JPA는 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 잘 지원하지 않는다. 하지만 때로는 특정 데이터베이스에 종속적인 기능이 필요할 수도 있다. 다양한 이유로 JPQL을 사용할 수 없을 때, JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 네이티브 SQL(네이티브쿼리)라 한다.
  • 즉, 사용자가 직접 데이터베이스에 날리는 쿼리를 작성하는 것이다.
  • NativeQuery는 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.
반응형

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

[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10
[JPA] Cascade  (1) 2023.09.10
[JPA] @OneToMany orphanRemoval  (0) 2023.09.10
[JPA] @OneToMany 단방향 매핑의 단점  (0) 2023.09.10
반응형

Cascade란?

  • cascade 옵션이란 @OneToMany나 @ManyToOne에 옵션으로 줄 수 있는 값이다.
  • Entity의 상태 변화를 전파시키는 옵션이다.
  • 만약 Entity의 상태 변화가 있으면 연관되어 있는(ex. @OneToMany, @ManyToOne) Entity에도 상태 변화를 전이시키는 옵션이다.
  • 기본적으로는 아무 것도 전이시키지 않는다.

Entity의 상태

  1. Transient
  • 객체를 생성하고 값을 주어도 JPA나 hibernate가 그 객체에 관해 아무것도 모르는 상태. 즉, 데이터베이스와 매핑된 것이 아무것도 없다.
  1. Persistent
  • 저장을 하고나서 JPA가 아는 상태(관리하는 상태)가 된다. 그러나 .save()를 했다고 해서 바로 DB에 데이터가 들어가는 것이 아니다. JPA가 persistent 상태로 관리하고 있다가 후에 데이터를 저장한다.(1차 캐시, Dirty Checking(변경사항 감지), Write Behind(최대한 늦게, 필요한 시점에 DB에 적용)등의 기능을 제공한다)
  1. Detached
  • JPA가 더이상 관리하지 않는 상태. JPA가 제공해주는 기능들을 사용하고 싶다면 다시 persistent 상태로 돌아가야한다.
  1. Removed
  • JPA가 관리하는 상태이긴 하지만 실제 commit이 일어날 때 삭제가 일어난다.

사용 예제

ex) Post(1) : Comment(N) 관계

게시글이 존재하고 해당 게시물에 달린 댓글들은 만약 이 게시글이 저장되거나 삭제되면 같이 저장되거나 삭제되어야 한다고 가정한다.

-> 이 상태로 실행하면 post만 저장해주었기 때문에 post 테이블에만 데이터가 저장된다.

여기서 Post 객체에 cascade 옵션을 주면 comment도 같이 저장이된다.

-> Post라는 인스턴스가 Transient에서 Persistent 상태로 넘어갈 때 child 객체(Comment)도 같이 Persisten 상태가 되면서 같이 저장이 되는 것이다.

cascade remove옵션도 주고나서 삭제하면 게시글을 지우면 댓글들도 같이 삭제된다.

일반적으로는 CascadeType.ALL 옵션을 줘서 사용한다.

반응형

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

[JPA] @MappedSuperclass  (0) 2023.09.10
[Spring] NativeQuery  (0) 2023.09.10
[JPA] @OneToMany orphanRemoval  (0) 2023.09.10
[JPA] @OneToMany 단방향 매핑의 단점  (0) 2023.09.10
JPA 무한 재귀 해결방법  (0) 2023.09.10
반응형

orphanRemoval

  • JPA 2.0 이상에서 지원하는 것으로 ORM 스팩, JPA 레벨에서의 정의입니다.
  • orphanRemoval은 @OneToMayn 연관에서 부모 엔티티의 컬렉션 등에서 자식 엔티티가 삭제될 때 참조가 끊어지므로 DB 레벨에서도 삭제되고 @OneToOne 연관에서 엔티티가 삭제될 때 연관된 엔티티 참조가 끊어지므로 DB에서 삭제된다. 즉 참조, 연결이 끊어진(Disconnected된) 엔티티를 같이 삭제하라는 의미로 Owner 객체와 참조가 끊어진 객체들을 정리할 때 유용하다.

ex)

@Entity
class Team{
    @OneToMany(orphanRemoval=true)
    private List<Member> members;
}
반응형

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

[Spring] NativeQuery  (0) 2023.09.10
[JPA] Cascade  (1) 2023.09.10
[JPA] @OneToMany 단방향 매핑의 단점  (0) 2023.09.10
JPA 무한 재귀 해결방법  (0) 2023.09.10
JPA 주요속성(@JoinColumn, @MaynToOne, @OneToMany)  (0) 2023.09.10
반응형

단점

  1. 엔티티가 관리하는 외래 키가 다른 테이블에 있음 -> 작업한 Entity가 아닌 다른 Entity에서 쿼리문이 나가는 경우가 있어 헷갈림
  2. 불필요한 쿼리문이 발생(update 등..)
  3. join table 문제

객체 저장시 update 쿼리문 추가 발생

  • Team(1) : Member(N) 구조를 가지는 테이블로 비교하겠습니다.
  • Team Entity에서 @OneToMany로 단방향을 가지는 구조입니다.

ex) Entity

ex) 2개의 member를 저장하는 코드

-> 실행 결과

member에 팀을 세팅해주는 2번의 update 쿼리가 추가로 발생함
만약 @ManyToOnE의 관계였다면 member 객체가 team을 가지고 있는 구조였다면 발생하지 않습니다.(외래키를 member가 직접 관리하기 때문에)

ex) team에 속한 member 삭제

members에서 member를 제거 했으니 해당 member에 team_id를 null로 만드는 update쿼리가 날라가는게 당연하고 memberRepository로 member를 제거했으니 delete 쿼리가 날라가는게 당연합니다.

@OneToMany 양방향

-> 위의 내용 수행 결과

ex) Team의 @OneToMany에 orphanRemoval과 cascade설정을 다음과 같이 해줍니다.

members에서 member를 삭제해주기만해도 memberRepository와 연동되어 자동으로 member를 삭제해줍니다.

반응형

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

[JPA] Cascade  (1) 2023.09.10
[JPA] @OneToMany orphanRemoval  (0) 2023.09.10
JPA 무한 재귀 해결방법  (0) 2023.09.10
JPA 주요속성(@JoinColumn, @MaynToOne, @OneToMany)  (0) 2023.09.10
JPA 지연로딩(LAZY) 즉시로딩(EAGER)  (0) 2023.09.10

+ Recent posts