본문 바로가기

Web/DB

[Hibernate orm] 11. Fetching

728x90

https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#fetching

 

Hibernate ORM 5.2.18.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

틀린 해석이나 내용이 있다면 알려주세요, 감사합니다 😊

 

 


11. Fetching

 

Fetching 은 db에서 데이터를 가져와 애플리케이션에서 사용할 수 있도록 하는 프로세스이다

이 수행 방식을 조정하는 것인데, 너무 많은 데이터를 가져오게 되어 불필요한 overhead가 생기거나 너무 적은 데이터를 가져와 추가 fetch 가 필요할 경우에 조정할 수 있다

 

 

11.1. The basics

Fetch의 개념은 두가지의 질문으로 나뉜다

1. 언제 데이터를 가져올까? 지금(eager or immediate)/나중에(lazy or delayed)

2. 어떻게 가져와야할까?

 

 

 

기본적인 DEFAULT 

 

static

Static definition of fetching strategies is done in the mappings. The statically-defined fetch strategies is used in the absence of any dynamically defined strategies

 

SELECT

Performs a separate SQL select to load the data. This can either be EAGER (the second select is issued immediately) or LAZY (the second select is delayed until the data is needed). This is the strategy generally termed N+1.

 

> EAGLE - 두번째 SELECT 가 즉시 실행 됨

> LAZY - 두번째 SELECT 가 db 요청 시에 실행 됨

 

JOIN

Inherently an EAGER style of fetching. The data to be fetched is obtained through the use of an SQL outer join.

BATCH

Performs a separate SQL select to load a number of related data items using an IN-restriction as part of the SQL WHERE-clause based on a batch size. Again, this can either be EAGER (the second select is issued immediately) or LAZY (the second select is delayed until the data is needed).

SUBSELECT

Performs a separate SQL select to load associated data based on the SQL restriction used to load the owner. Again, this can either be EAGER (the second select is issued immediately) or LAZY (the second select is delayed until the data is needed).

dynamic (sometimes referred to as runtime)

실제 런타임 상태 중심

Dynamic definition is really use-case centric. There are multiple ways to define dynamic fetching:

fetch profiles

defined in mappings, but can be enabled/disabled on the Session.

HQL/JPQL

and both Hibernate and JPA Criteria queries have the ability to specify fetching, specific to said query.

entity graphs

Starting in Hibernate 4.2 (JPA 2.1) this is also an option.

 

 

 

 

11.2. Direct fetching vs entity queries

 

예제가 이렇게 있다고 생각 하고 진행한다

@Entity(name = "Department")
public static class Department {

    @Id
    private Long id;

    //Getters and setters omitted for brevity
}

@Entity(name = "Employee")
public static class Employee {

    @Id
    private Long id;

    @NaturalId
    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    private Department department;

    //Getters and setters omitted for brevity
}

 

 eagerly 방식으로 fetch를 할 경우에는, Employee 의 Department 정보를 가지고 오겠다는 뜻이다

 

Hibernate는 이와 같은 쿼리로 데이터를 불러온다

 

Employee employee = entityManager.find( Employee.class, 1L );

 

select
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.username as username2_1_0_,
    d.id as id1_0_1_
from
    Employee e
left outer join
    Department d
        on e.department_id=d.id
where
    e.id = 1

 

보면 이미 LEFT JOIN 을 처리하여 가져오고 있다

만약 JOIN FETCH 처리가 없다면

 

Employee employee = entityManager.createQuery(
        "select e " +
        "from Employee e " +
        "where e.id = :id", Employee.class)
.setParameter( "id", 1L )
.getSingleResult();

 

select
    e.id as id1_1_,
    e.department_id as departme3_1_,
    e.username as username2_1_
from
    Employee e
where
    e.id = 1

select
    d.id as id1_0_0_
from
    Department d
where
    d.id = 1

 

 

이렇게 가져오게 될 것이다.

 

EAGER 연결을 할 경우에 Hibernate 는 N + 1 쿼리 문제가 있을 것이다.

그래서 보통 LAZY 방식을 선호한다

 

 

 

11.3. Applying fetch strategies

Example 388. Sample domain model
@Entity(name = "Department")
public static class Department {

    @Id
    private Long id;

    @OneToMany(mappedBy = "department")
    private List<Employee> employees = new ArrayList<>();

    //Getters and setters omitted for brevity
}

@Entity(name = "Employee")
public static class Employee {

    @Id
    private Long id;

    @NaturalId
    private String username;

    @Column(name = "pswd")
    @ColumnTransformer(
        read = "decrypt( 'AES', '00', pswd  )",
        write = "encrypt('AES', '00', ?)"
    )
    private String password;

    private int accessLevel;

    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;

    @ManyToMany(mappedBy = "employees")
    private List<Project> projects = new ArrayList<>();

    //Getters and setters omitted for brevity
}

@Entity(name = "Project")
public class Project {

    @Id
    private Long id;

    @ManyToMany
    private List<Employee> employees = new ArrayList<>();

    //Getters and setters omitted for brevity
}

The Hibernate recommendation is to statically mark all associations lazy and to use dynamic fetching strategies for eagerness.

 

Hibernate는 모두 LAZY로 적용하고 필요할 때만 EAGER을 쓰는 것을 권장한다

그렇기 때문에 onetoone , manytoone 의 JPA 사양과 부딪히게 된다

하지만 JPA는 Hibernate 의 기본값을 받아들인다

 

 

 

11.4. No fetching

만약 Employee 예제에서 로그인에 대한 정보만 필요할 경우가 있을 때를 예시로 들어본다
Employee employee = entityManager.createQuery(
    "select e " +
    "from Employee e " +
    "where " +
    "    e.username = :username and " +
    "    e.password = :password",
    Employee.class)
.setParameter( "username", username)
.setParameter( "password", password)
.getSingleResult();

 

현재 Employee Fetch가 LAZY 상태이기 때문에 다른 데이터를 가져오지 않는다 => 최적화 됨

 

Example 390. No fetching (scalar) example

Integer accessLevel = entityManager.createQuery(
    "select e.accessLevel " +
    "from Employee e " +
    "where " +
    "    e.username = :username and " +
    "    e.password = :password",
    Integer.class)
.setParameter( "username", username)
.setParameter( "password", password)
.getSingleResult();

 

 

11.5. Dynamic fetching via queries

이번 예제는 Employee 에 대한 화면을 표시할 경우이다, 이럴 경우에는 Employee 데이터에 대한 Acess는 필요하지만, 부서나 다른 직원 정보는 필요가 없다

 

Example 391. Dynamic JPQL fetching example
Employee employee = entityManager.createQuery(
    "select e " +
    "from Employee e " +
    "left join fetch e.projects " +
    "where " +
    "    e.username = :username and " +
    "    e.password = :password",
    Employee.class)
.setParameter( "username", username)
.setParameter( "password", password)
.getSingleResult();

 

Example 392. Dynamic query fetching example
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> query = builder.createQuery( Employee.class );
Root<Employee> root = query.from( Employee.class );
root.fetch( "projects", JoinType.LEFT);
query.select(root).where(
    builder.and(
        builder.equal(root.get("username"), username),
        builder.equal(root.get("password"), password)
    )
);
Employee employee = entityManager.createQuery( query ).getSingleResult();

 

이 예제의 경우 모든 정보를 얻기 위해 동적으로 가져온다

 

 

 

 

 

11.6. Dynamic fetching via JPA entity graph

JPA 2.1 introduced entity graphs so the application developer has more control over fetch plans.

fetch plan을 위한 엔티티 그래프

 

Example 393. Fetch graph example

@Entity(name = "Employee")
@NamedEntityGraph(name = "employee.projects",
    attributeNodes = @NamedAttributeNode("projects")
)
Employee employee = entityManager.find(
    Employee.class,
    userId,
    Collections.singletonMap(
        "javax.persistence.fetchgraph",
        entityManager.getEntityGraph( "employee.projects" )
    )
);

 

When executing a JPQL query, if an EAGER association is omitted, Hibernate will issue a secondary select for every association needed to be fetched eagerly, which can lead dto N+1 query issues. For this reason, it’s better to use LAZY associations, and only fetch them eagerly on a per-query basis.

 

 

11.6.1. JPA entity subgraphs

Entity Graph는 속성을 지정하지만 단일 엔티티로만 제한된다

자식 엔티티를 가져오려면 @NamedSubgraph애노테이션을 사용해야한다

 

 

Example 394. Fetch graph with a subgraph mapping
@Entity(name = "Project")
@NamedEntityGraph(name = "project.employees",
    attributeNodes = @NamedAttributeNode(
        value = "employees",
        subgraph = "project.employees.department"
    ),
    subgraphs = @NamedSubgraph(
        name = "project.employees.department",
        attributeNodes = @NamedAttributeNode( "department" )
    )
)
public static class Project {

    @Id
    private Long id;

    @ManyToMany
    private List<Employee> employees = new ArrayList<>();

    //Getters and setters omitted for brevity
}

 

Example 395. Fetch graph with a subgraph mapping

Project project = doInJPA( this::entityManagerFactory, entityManager -> {
    return entityManager.find(
        Project.class,
        1L,
        Collections.singletonMap(
            "javax.persistence.fetchgraph",
            entityManager.getEntityGraph( "project.employees" )
        )
    );
} );
select
    p.id as id1_2_0_, e.id as id1_1_1_, d.id as id1_0_2_,
    e.accessLevel as accessLe2_1_1_,
    e.department_id as departme5_1_1_,
    decrypt( 'AES', '00', e.pswd  ) as pswd3_1_1_,
    e.username as username4_1_1_,
    p_e.projects_id as projects1_3_0__,
    p_e.employees_id as employee2_3_0__
from
    Project p
inner join
    Project_Employee p_e
        on p.id=p_e.projects_id
inner join
    Employee e
        on p_e.employees_id=e.id
inner join
    Department d
        on e.department_id=d.id
where
    p.id = ?

-- binding parameter [1] as [BIGINT] - [1]

 

 

 

11.7. Dynamic fetching via Hibernate profiles

만약 정보를 가져올 때 natural-id 를 활용하고 싶을 때의 요청이다

 

Example 396. Fetch profile example
@Entity(name = "Employee")
@FetchProfile(
    name = "employee.projects",
    fetchOverrides = {
        @FetchProfile.FetchOverride(
            entity = Employee.class,
            association = "projects",
            mode = FetchMode.JOIN
        )
    }
)
session.enableFetchProfile( "employee.projects" );
Employee employee = session.bySimpleNaturalId( Employee.class ).load( username );

 

Employee 에서 natural-Id 를 얻은 뒤 그 데이터로 Project 데이터를 가져온다

Employee 데이터가 캐시에서 확인되면 Project는 자체적으로 확인된다

만약 캐시가 해결되지 않으면 join을 통해 해결한다

 

11.8. Batch fetching

초기화 되지 않은 Entity Proxy 를 가져오는 애노테이션이다

 

Hibernate offers the @BatchSize annotation, which can be used when fetching uninitialized entity proxies.

Considering the following entity mapping:

Example 397. @BatchSize mapping example
@Entity(name = "Department")
public static class Department {

    @Id
    private Long id;

    @OneToMany(mappedBy = "department")
    //@BatchSize(size = 5)
    private List<Employee> employees = new ArrayList<>();

    //Getters and setters omitted for brevity

}

@Entity(name = "Employee")
public static class Employee {

    @Id
    private Long id;

    @NaturalId
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;

    //Getters and setters omitted for brevity
}

 

 

Considering that we have previously fetched several Department entities, and now we need to initialize the employees entity collection for each particular Department, the @BatchSize annotations allows us to load multiple Employee entities in a single database roundtrip.

Example 398. @BatchSize fetching example
List<Department> departments = entityManager.createQuery(
    "select d " +
    "from Department d " +
    "inner join d.employees e " +
    "where e.name like 'John%'", Department.class)
.getResultList();

for ( Department department : departments ) {
    log.infof(
        "Department %d has {} employees",
        department.getId(),
        department.getEmployees().size()
    );
}
SELECT
    d.id as id1_0_
FROM
    Department d
INNER JOIN
    Employee employees1_
    ON d.id=employees1_.department_id

SELECT
    e.department_id as departme3_1_1_,
    e.id as id1_1_1_,
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.name as name2_1_0_
FROM
    Employee e
WHERE
    e.department_id IN (
        0, 2, 3, 4, 5
    )

SELECT
    e.department_id as departme3_1_1_,
    e.id as id1_1_1_,
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.name as name2_1_0_
FROM
    Employee e
WHERE
    e.department_id IN (
        6, 7, 8, 9, 1
    )

As you can see in the example above, there are only two SQL statements used to fetch the Employee entities associated to multiple Department entities.

 

 

@BatchSize 가 없다면 이 여러 항목들을 가져올 때 10개의 쿼리가 필요하다

하지만 JOIN FETCH로 하면 더 낫다

 

 

11.9. The @Fetch annotation mapping

Besides the FetchType.LAZY or FetchType.EAGER JPA annotations, you can also use the Hibernate-specific @Fetch annotation that accepts one of the following FetchMode(s):

SELECT

The association is going to be fetched lazily using a secondary select for each individual entity, collection, or join load. It’s equivalent to JPA FetchType.LAZY fetching strategy.

 

JOIN

Use an outer join to load the related entities, collections or joins when using direct fetching. It’s equivalent to JPA FetchType.EAGER fetching strategy.

 

SUBSELECT

Available for collections only. When accessing a non-initialized collection, this fetch mode will trigger loading all elements of all collections of the same role for all owners associated with the persistence context using a single secondary select.

 

11.10. FetchMode.SELECT

To demonstrate how FetchMode.SELECT works, consider the following entity mapping:

 

LAZY 시키기 위해 FetchMode.SELECT 매핑

Example 399. FetchMode.SELECT mapping example
@Entity(name = "Department")
public static class Department {

    @Id
    private Long id;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    @Fetch(FetchMode.SELECT)
    private List<Employee> employees = new ArrayList<>();

    //Getters and setters omitted for brevity

}

@Entity(name = "Employee")
public static class Employee {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;

    //Getters and setters omitted for brevity

}

Considering there are multiple Department entities, each one having multiple Employee entities, when executing the following test case, Hibernate fetches every uninitialized Employee collection using a secondary SELECT statement upon accessing the child collection for the first time:

 

Example 400. FetchMode.SELECT mapping example
List<Department> departments = entityManager.createQuery(
    "select d from Department d", Department.class )
.getResultList();

log.infof( "Fetched %d Departments", departments.size());

for (Department department : departments ) {
    assertEquals( 3, department.getEmployees().size() );
}
SELECT
    d.id as id1_0_
FROM
    Department d

-- Fetched 2 Departments

SELECT
    e.department_id as departme3_1_0_,
    e.id as id1_1_0_,
    e.id as id1_1_1_,
    e.department_id as departme3_1_1_,
    e.username as username2_1_1_
FROM
    Employee e
WHERE
    e.department_id = 1

SELECT
    e.department_id as departme3_1_0_,
    e.id as id1_1_0_,
    e.id as id1_1_1_,
    e.department_id as departme3_1_1_,
    e.username as username2_1_1_
FROM
    Employee e
WHERE
    e.department_id = 2

The more Department entities are fetched by the first query, the more secondary SELECT statements are executed to initialize the employees collections.

Therefore, FetchMode.SELECT can lead to N+1 query issues.

 

SELECT 쿼리에서 더 많은 엔티티를 가져올 수록 보조문이 많이 실행 된다 이러면 N+1 문제가 생길 수 있다

 

 

11.11. FetchMode.SUBSELECT

To demonstrate how FetchMode.SUBSELECT works, we are going to modify the FetchMode.SELECT mapping example to use FetchMode.SUBSELECT:

 

매핑을 FetchMode.SUBSELECT 로 수정하면

 

Example 401. FetchMode.SUBSELECT mapping example
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private List<Employee> employees = new ArrayList<>();

Now, we are going to fetch all Department entities that match a given filtering criteria and then navigate their employees collections.

Hibernate is going to avoid the N+1 query issue by generating a single SQL statement to initialize all employees collections for all Department entities that were previously fetched. Instead of using passing all entity identifiers, Hibernate simply reruns the previous query that fetched the Department entities.

 

 

Example 402. FetchMode.SUBSELECT mapping example
List<Department> departments = entityManager.createQuery(
    "select d " +
    "from Department d " +
    "where d.name like :token", Department.class )
.setParameter( "token", "Department%" )
.getResultList();

log.infof( "Fetched %d Departments", departments.size());

for (Department department : departments ) {
    assertEquals( 3, department.getEmployees().size() );
}
SELECT
    d.id as id1_0_
FROM
    Department d
where
    d.name like 'Department%'

-- Fetched 2 Departments

SELECT
    e.department_id as departme3_1_1_,
    e.id as id1_1_1_,
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.username as username2_1_0_
FROM
    Employee e
WHERE
    e.department_id in (
        SELECT
            fetchmodes0_.id
        FROM
            Department fetchmodes0_
        WHERE
            d.name like 'Department%'
    )

 

 

11.12. FetchMode.JOIN

To demonstrate how FetchMode.JOIN works, we are going to modify the FetchMode.SELECT mapping example to use FetchMode.JOIN instead:

Example 403. FetchMode.JOIN mapping example
@OneToMany(mappedBy = "department")
@Fetch(FetchMode.JOIN)
private List<Employee> employees = new ArrayList<>();

Now, we are going to fetch one Department and navigate its employees collections.

이렇게 하면 employee 하나를 가져온 다음에 그에 대한 Department를 조회할 것이다

 

 

JPQL 쿼리를 이용하여 여러 Department의 엔티티를 가져올 때 FetchMode.JOIN 을 사용하면 query fetching이 가 재정의된다

JPQL 쿼리로 여러 관게를 가져올 경우엔 JOIN FETCH 를 사용해야한다

따라서 FetchMode.JOIN 을 사용하려면 식별자나 natural ID 가 있어서 이를 통해 직접 가져올 때 유용하다

 

FetchMode.JOIN은 EAGER 이 디폴트이고, LAZY로 뒀다해도 계속 연결을 시도한다(재정의 되므로)

 

 

(위의 식별자를 이용한 데이터 가져올 때 말고는 쓰지말라는 뜻)

Hibernate is going to avoid the secondary query by issuing an OUTER JOIN for the employees collection.

Example 404. FetchMode.JOIN mapping example
Department department = entityManager.find( Department.class, 1L );

log.infof( "Fetched department: %s", department.getId());

assertEquals( 3, department.getEmployees().size() );
SELECT
    d.id as id1_0_0_,
    e.department_id as departme3_1_1_,
    e.id as id1_1_1_,
    e.id as id1_1_2_,
    e.department_id as departme3_1_2_,
    e.username as username2_1_2_
FROM
    Department d
LEFT OUTER JOIN
    Employee e
        on d.id = e.department_id
WHERE
    d.id = 1

-- Fetched department: 1

This time, there was no secondary query because the child collection was loaded along with the parent entity.

 

 

 

 

11.13. @LazyCollection

The @LazyCollection annotation is used to specify the lazy fetching behavior of a given collection. The possible values are given by the LazyCollectionOption enumeration:

@LazyCollection 을 사용 하면 LAZY 동작을 지정할 수 있다 

 

TRUE
Load it when the state is requested.

FALSE
Eagerly load it.

EXTRA
Prefer extra queries over full collection loading.

The TRUE and FALSE values are deprecated since you should be using the JPA FetchType attribute of the @ElementCollection, @OneToMany, or @ManyToMany collection.

The EXTRA value has no equivalent in the JPA specification, and it’s used to avoid loading the entire collection even when the collection is accessed for the first time. Each element is fetched individually using a secondary query.

EXTRA 는 처음 액세스 할 경우 전체 컬렉션 로드를 방지할 수 있다.

 

 

 

Example 405. LazyCollectionOption.EXTRA mapping example
@Entity(name = "Department")
public static class Department {

    @Id
    private Long id;

    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
    @OrderColumn(name = "order_id")
    @LazyCollection( LazyCollectionOption.EXTRA )
    private List<Employee> employees = new ArrayList<>();

    //Getters and setters omitted for brevity

}

@Entity(name = "Employee")
public static class Employee {

    @Id
    private Long id;

    @NaturalId
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;

    //Getters and setters omitted for brevity

}

 

 

LazyCollectionOption.EXTRA 

only works for ordered collections, either List(s) that are annotated with @OrderColumn or Map(s). 

이 애노테이션은 @OrderColumn 으로 주석이 달린 컬렉션에 대해서만 사용한다

 

For bags (e.g. regular List(s) of entities that do not preserve any certain ordering), the @LazyCollection(LazyCollectionOption.EXTRA)` behaves like any other FetchType.LAZY collection (the collection is fetched entirely upon being accessed for the first time).

 

 

Example 406. LazyCollectionOption.EXTRA Domain Model example
Department department = new Department();
department.setId( 1L );
entityManager.persist( department );

for (long i = 1; i <= 3; i++ ) {
    Employee employee = new Employee();
    employee.setId( i );
    employee.setUsername( String.format( "user_%d", i ) );
    department.addEmployee(employee);
}

When fetching the employee collection entries by their position in the List, Hibernate generates the following SQL statements:

 

 

Example 407. LazyCollectionOption.EXTRA fetching example
Department department = entityManager.find(Department.class, 1L);

int employeeCount = department.getEmployees().size();

for(int i = 0; i < employeeCount; i++ ) {
    log.infof( "Fetched employee: %s", department.getEmployees().get( i ).getUsername());
}
SELECT
    max(order_id) + 1
FROM
    Employee
WHERE
    department_id = ?

-- binding parameter [1] as [BIGINT] - [1]

SELECT
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.username as username2_1_0_
FROM
    Employee e
WHERE
    e.department_id=?
    AND e.order_id=?

-- binding parameter [1] as [BIGINT]  - [1]
-- binding parameter [2] as [INTEGER] - [0]

SELECT
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.username as username2_1_0_
FROM
    Employee e
WHERE
    e.department_id=?
    AND e.order_id=?

-- binding parameter [1] as [BIGINT]  - [1]
-- binding parameter [2] as [INTEGER] - [1]

SELECT
    e.id as id1_1_0_,
    e.department_id as departme3_1_0_,
    e.username as username2_1_0_
FROM
    Employee e
WHERE
    e.department_id=?
    AND e.order_id=?

-- binding parameter [1] as [BIGINT]  - [1]
-- binding parameter [2] as [INTEGER] - [2]

 

이렇게 하면 전체 컬렉션을 trigger 하지 않고 자식 엔티티를 차례로 가져올 수 있다

LazyCollectionOption.EXTRA 는 N+1 문제가 생길 수 있어 주의해야한다

728x90