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
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.
BATCHPerforms 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).
SUBSELECTPerforms 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:
defined in mappings, but can be enabled/disabled on the Session.
HQL/JPQLand both Hibernate and JPA Criteria queries have the ability to specify fetching, specific to said query.
entity graphsStarting 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
@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 = 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는 필요하지만, 부서나 다른 직원 정보는 필요가 없다
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();
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애노테이션을 사용해야한다
@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 를 활용하고 싶을 때의 요청이다
@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:
@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.
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):
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 매핑
@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:
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 로 수정하면
@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.
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:
@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.
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 동작을 지정할 수 있다
FALSE
EXTRA
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 는 처음 액세스 할 경우 전체 컬렉션 로드를 방지할 수 있다.
@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).
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:
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 문제가 생길 수 있어 주의해야한다
'Web > DB' 카테고리의 다른 글
[Spring postgreSQL] 연동하기 (0) | 2023.03.17 |
---|---|
[SQL] 정규화(Normalization) (0) | 2023.03.05 |
[Spring framework Data JPA] 5.1.5. Specifications (0) | 2023.02.09 |
[Hibernate validator] 5. Grouping constraints (0) | 2023.02.06 |
[Hibernate validator] 6. Creating custom constraints (1) | 2023.02.02 |