PlayData 백엔드 부트캠프 정리

다시 시작하는 부트 캠프 하루 후기 5일차

효건 2024. 10. 18. 17:09

<JPA 연관관계 설정 >

JPA의 연관 관계 설정이 객체 지향 프로그래밍과 관계형 데이터베이스의 차이를 극복하기 위한 방법입니다.

- @ONE TO ONE

한 엔티티가 다른 엔티티와 1:1로 연결된 경우에 사용됩니다.

 

- @ONE TO MANY

한 엔티티가 여러 엔티티와 연결된 경우에 사용됩니다.

// 양방향 맵핑에서는 실제 테이블에 List가 세팅되지 않습니다.
    // 엔터티 안에서만 사용하는 가상의 컬럼입니다.
    // 상대방 엔터티의 갱신에 관여할 수 없기 때문에 단순히 읽기 전용(조회)으로만 사용하는 것을 권장.
@OneToMany(mappedBy = "department") // 연관관계 엔터티의 필드명을 작성.
private List<Employee> employees;

- @MANY TO ONE

여러 엔티티가 한 엔티티와 연결된 경우에 사용됩니다.

 

- 단 방향 연관 관계

한 쪽 엔티티만 다른 쪽 엔티티를 참조하는 관계입니다. 이를 통해 해당 엔티티는 관련된 엔티티를 조회할 수 있지만, 반대 방향으로는 조회할 수 없습니다.

 

- 양 방향 연관 관계

양쪽 엔티티 모두 관련된 엔티티를 조회할 수 있습니다.

// 연관관계 편의 메서드 (양방향에서 연관 필드가 수정될 경우
// 실제 테이블과의 데이터를 맞춰주기 위한 메서드 선언)
public void changeDepartment(Department department) {
    this.department = department;
    department.getEmployees().add(this);
}

 

- JPA Lazy Loading vs Eager Loading

// EAGER: 해당 필드를 사용 하든 말든 항상 무조건 조인을 수행
// LAZY: 필요한 경우에만 데이터를 가져온다. -> 실무
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id") // FK 컬럼명 (연관 테이블의 컬럼명과 일치하게)

LAZY가 많이 쓰이닌 확인할필요가 있을듯 하고 FK의 개념을 다시한번 살펴볼 필요가 있다.

- N+1 문제 및 Fetch Join

N+1 문제는 ORM(Object-Relational Mapping)에서 발생하는 성능 문제로, 연관된 엔티티를 처리할 때 발생합니다. 'N+1'이라는 이름은 데이터베이스에 실행되는 쿼리의 수를 나타냅니다.

 

Fetch Join은 JPQL(JPA Query Language)의 JOIN FETCH 구문을 사용하여 수행됩니다. 다음은 User 엔티티와 연관된 Post 엔티티를 Fetch Join으로 가져오는 예입니다.

큰 데이터 세트의 경우, 한 번에 많은 데이터를 가져오면 메모리를 과도하게 사용하게 되므로, Fetch Join은 적절하게 사용해야 합니다.

 

- 고아 객체(Orphan Removal)

	@Test
    @DisplayName("고아 객체 삭제")
    void orphanRemovalTest() {
        // given
        // 2번 부서 조회
        Department department
                = departmentRepository.findById(2L).orElseThrow();
        // 2번 부서 사원 목록 가져오기
        List<Employee> employeeList = department.getEmployees();
        // when
        Employee employee = employeeList.get(1);
        employeeList.remove(employee);
        // then
    }

- CascadeType

// 양방향 맵핑에서는 실제 테이블에 List가 세팅되지 않습니다.
// 엔터티 안에서만 사용하는 가상의 컬럼입니다.
// 상대방 엔터티의 갱신에 관여할 수 없기 때문에 단순히 읽기 전용(조회)으로만 사용하는 것을 권장.
@OneToMany(mappedBy = "department", // 연관관계 엔터티의 필드명을 작성.
        cascade = PERSIST, orphanRemoval=true)
private List<Employee> employees;

 

 

다대다는 왠만하면 사용하지 않는 것으로 해야합니다. 따라서 데이터베이스 정규화 과정을 통해 다대다 방식을 없애면서 하는 것이 상당히 이롭다. 정신적이나 신체적으로 매우 유리하다고 볼수있다. 

다대다가 생기면 모델링과정으로 다시 돌아가는 것이 좋다.

 

<queryDSL>

- 초기 설정 : build.gradle에 아래와 같이 넣은 후 others 폴더의 compile.java를 실행시키면 

generate 파일이 생기는 것을 볼수있다.

    // queryDSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"


}

tasks.named('test') {
    useJUnitPlatform()
}

/**
 //querydsl 추가 시작
 //queryDsl은 내부적으로 Entity 클래스를 인식해서 그와 비슷한 모양의 QClass를 제작합니다.
 //이 QClass를 이용해서 Entity의 타입을 검증할 수 있고, 그에 맞는 쿼리문을 builder 형태로
 //제작합니다.
 //밑에 작성된 내용은 QClass의 생성 디렉토리 지정 및 프로젝트 build 시 컴파일 명령문 등을
 //세팅해 놓은 코드입니다. */
def querydslDir = "src/main/generated"

sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
    file(querydslDir).deleteDir()
}

 

config 파일을 만들어서 아래와 같이 설정한다.

// QueryDSL 세팅
@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager em;
    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

그러면 JPAQueryFactory를 사용할수있게 되었다.

아래와 같이 조회해올수있다.

        String jpqlQuery = "SELECT i FROM Idol i WHERE i.(조회할  칼럼) = ?1";
        //when
        Idol foundIdol = em.createQuery(jpqlQuery, Idol.class)
                .setParameter(1, "가을")
                .getSingleResult();

 

그리고 

Idol foundIdol = factory
        .select(idol)
        .from(idol)
        .where(
                idol.idolName.eq("리즈")
                        .and(idol.age.eq(20))
        )
        .fetchOne();
// fetchOne: 단일 건 조회. 여러 건 조회시 예외 발생.
// fetchFirst: 단일 건 조회. 여러 건 조회되어도 첫번째 값만 반환.
// fetch: List 형태로 반환.

 

와같이 메서드를 사용할 수있다. 

 

- 메서드 커스텀해서 사용하기 

@Override
public List<Idol> findAllSortedByName() {
    return  factory
            .select(idol)
            .from(idol)
            .orderBy(idol.idolName.asc())
            .fetch();
}

오버라이드 쓰면 된다고 합니다 ㅎㅎ