@Entity
@Table(name = "categorys")
public class Category {
@Id
@GeneratedValue
private Long id;
private String name;
private String description1;
private String description2;
private String description3;
@OneToMany(mappedBy = "category", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
}
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private Double price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
}
Category
는 여러 Product
를 가질 수 있습니다.queryFactory
.select(Projections.fields(ProductDetails2.class,
product.id.as("productId"),
product.name.as("productName"),
product.price.as("productPrice"),
product.category.id.as("categoryId")))
.from(product)
.where(product.price.gt(0))
.orderBy(product.name.asc())
.offset(offset)
.limit(pageSize)
.fetch();
OFFSET
키워드를 사용하는 페이징 쿼리에서, 데이터베이스는 주어진 OFFSET
값까지의 모든 레코드를 읽고, 그 다음에 나오는 LIMIT
에 지정된 수만큼의 레코드만 반환한다.OFFSET
이 1000000이고 LIMIT
이 10일 경우, 데이터베이스는 첫 1000000개 레코드를 모두 읽은 후 다음 10개 레코드를 반환한다.OFFSET
값이 커지고, 따라서 데이터베이스가 무시해야 하는 레코드의 수가 많아진다.@Test
@DisplayName("Product 안에 category Id 만 가져오기 with Pagination (No Offset)")
public void test5() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
int pageSize = 20; // 페이지 당 항목 수 (예: 20)
Long productId = 1000000L; // 이전 페이지의 마지막 ID
List<ProductDetails2> results = queryFactory
.select(Projections.fields(ProductDetails2.class,
product.id.as("productId"),
product.name.as("productName"),
product.price.as("productPrice"),
product.category.id.as("categoryId")))
.from(product)
.where(product.price.gt(0), ltProductId(productId)) // No Offset 조건 추가
.orderBy(product.id.desc()) // ID 기준 정렬
.limit(pageSize)
.fetch();
}
private BooleanExpression ltProductId(Long productId) {
if (productId == null) {
return null; // BooleanExpression 자리에 null이 반환되면 조건문에서 자동으로 제거된다.
}
return product.id.lt(productId);
}
OFFSET
를 사용하여 페이지 번호에 기반한 데이터 위치를 결정한다다. 예를 들어, 페이지 번호가 100,000이고 페이지 크기가 20이면, OFFSET
는 1,999,980 ((100000 - 1) * 20
)이 된다. 이는 데이터베이스가 1,999,980 레코드를 건너뛴 후 데이터를 조회한다는 것을 의미한다.OFFSET
대신 마지막 조회 ID를 사용하여 다음 페이지의 데이터를 조회한다. 이 방식은 데이터베이스가 이전 페이지의 끝 ID부터 시작하여 데이터를 읽기 시작하므로, 불필요한 레코드를 건너뛸 필요가 없다.OFFSET
와 LIMIT
키워드를 사용한다.OFFSET
대신 특정 조건 (ltProductId(productId)
)을 사용하여 데이터를 조회한다.Hibernate:
select
product0_.id as col_0_0_,
product0_.name as col_1_0_,
product0_.price as col_2_0_,
product0_.category_id as col_3_0_
from
products product0_
where
product0_.price > ?
and product0_.id < ?
order by
product0_.id desc limit ?
@Test
@DisplayName("Product 안에 category Id 만 가져오기 with Covering Index")
public void test6() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QProduct product = QProduct.product;
int pageNumber = 1000000; // 페이지 번호
int pageSize = 20; // 페이지 당 항목 수
long offset = (pageNumber - 1) * pageSize; // 페이지 시작 위치 계산
// 첫 번째 쿼리: ID만 조회
List<Long> productIds = queryFactory
.select(product.id)
.from(product)
.where(product.price.gt(0))
.orderBy(product.name.asc())
.offset(offset)
.limit(pageSize)
.fetch();
// 두 번째 쿼리: 실제 데이터 조회
List<ProductDetails2> results = queryFactory
.select(Projections.fields(ProductDetails2.class,
product.id.as("productId"),
product.name.as("productName"),
product.price.as("productPrice"),
product.category.id.as("categoryId")))
.from(product)
.where(product.id.in(productIds))
.fetch();
}
// 첫 번째 쿼리: ID만 조회
Hibernate:
select
product0_.id as col_0_0_
from
products product0_
where
product0_.price>?
order by
product0_.name asc limit ?
// 두 번째 쿼리: 실제 데이터 조회
Hibernate:
select
product0_.id as col_0_0_,
product0_.name as col_1_0_,
product0_.price as col_2_0_,
product0_.category_id as col_3_0_
from
products product0_
where
product0_.id in (
? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ?
)