Karena ini adalah pertanyaan yang sangat umum, saya menulis
artikel ini , yang menjadi dasar jawaban ini.
Apa masalah permintaan N + 1
Masalah permintaan N + 1 terjadi ketika kerangka kerja akses data dieksekusi N pernyataan SQL tambahan untuk mengambil data yang sama yang bisa diambil ketika mengeksekusi query SQL primer.
Semakin besar nilai N, semakin banyak permintaan akan dieksekusi, semakin besar dampak kinerja. Dan, tidak seperti log kueri lambat yang dapat membantu Anda menemukan kueri berjalan lambat, masalah N +1 tidak akan spot karena setiap kueri tambahan individu berjalan cukup cepat untuk tidak memicu log kueri lambat.
Masalahnya adalah mengeksekusi sejumlah besar pertanyaan tambahan yang, secara keseluruhan, membutuhkan waktu yang cukup untuk memperlambat waktu respons.
Mari kita pertimbangkan kita memiliki tabel database post dan post_comments berikut yang membentuk hubungan tabel satu-ke-banyak :
Kami akan membuat 4 post
baris berikut :
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)
Dan, kami juga akan membuat 4 post_comment
catatan anak:
INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)
INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)
INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)
INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)
N + 1 masalah permintaan dengan SQL biasa
Jika Anda memilih post_comments
menggunakan kueri SQL ini:
List<Tuple> comments = entityManager.createNativeQuery("""
SELECT
pc.id AS id,
pc.review AS review,
pc.post_id AS postId
FROM post_comment pc
""", Tuple.class)
.getResultList();
Dan, nanti, Anda memutuskan untuk mengambil yang terkait post
title
untuk masing-masing post_comment
:
for (Tuple comment : comments) {
String review = (String) comment.get("review");
Long postId = ((Number) comment.get("postId")).longValue();
String postTitle = (String) entityManager.createNativeQuery("""
SELECT
p.title
FROM post p
WHERE p.id = :postId
""")
.setParameter("postId", postId)
.getSingleResult();
LOGGER.info(
"The Post '{}' got this review '{}'",
postTitle,
review
);
}
Anda akan memicu masalah permintaan N + 1 karena, alih-alih satu permintaan SQL, Anda mengeksekusi 5 (1 + 4):
SELECT
pc.id AS id,
pc.review AS review,
pc.post_id AS postId
FROM post_comment pc
SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
Memperbaiki masalah permintaan N + 1 sangat mudah. Yang perlu Anda lakukan adalah mengekstrak semua data yang Anda butuhkan dalam query SQL asli, seperti ini:
List<Tuple> comments = entityManager.createNativeQuery("""
SELECT
pc.id AS id,
pc.review AS review,
p.title AS postTitle
FROM post_comment pc
JOIN post p ON pc.post_id = p.id
""", Tuple.class)
.getResultList();
for (Tuple comment : comments) {
String review = (String) comment.get("review");
String postTitle = (String) comment.get("postTitle");
LOGGER.info(
"The Post '{}' got this review '{}'",
postTitle,
review
);
}
Kali ini, hanya satu query SQL yang dijalankan untuk mengambil semua data yang kami tertarik untuk menggunakannya.
Masalah permintaan N + 1 dengan JPA dan Hibernate
Saat menggunakan JPA dan Hibernate, ada beberapa cara Anda dapat memicu masalah permintaan N + 1, jadi sangat penting untuk mengetahui bagaimana Anda bisa menghindari situasi ini.
Untuk contoh berikutnya, pertimbangkan kami memetakan post
dan post_comments
tabel ke entitas berikut:
Pemetaan JPA terlihat seperti ini:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
//Getters and setters omitted for brevity
}
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
@Id
private Long id;
@ManyToOne
private Post post;
private String review;
//Getters and setters omitted for brevity
}
FetchType.EAGER
Menggunakan FetchType.EAGER
baik secara implisit atau eksplisit untuk asosiasi JPA Anda adalah ide yang buruk karena Anda akan mengambil lebih banyak data yang Anda butuhkan. Selain itu, FetchType.EAGER
strategi ini juga rentan terhadap masalah permintaan N +1.
Sayangnya, @ManyToOne
dan @OneToOne
asosiasi digunakan FetchType.EAGER
secara default, jadi jika pemetaan Anda terlihat seperti ini:
@ManyToOne
private Post post;
Anda menggunakan FetchType.EAGER
strategi, dan, setiap kali Anda lupa menggunakan JOIN FETCH
ketika memuat beberapa PostComment
entitas dengan permintaan JPQL atau API Kriteria:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Anda akan memicu masalah kueri N +1:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
Perhatikan pernyataan SELECT tambahan yang dieksekusi karena post
asosiasi harus diambil sebelum mengembalikan List
dariPostComment
entitas.
Tidak seperti rencana pengambilan default, yang Anda gunakan saat memanggil find
metode EnrityManager
, kueri JPQL atau Kriteria API menentukan rencana eksplisit yang tidak dapat diubah Hibernate dengan menyuntikkan JOIN FETCH secara otomatis. Jadi, Anda perlu melakukannya secara manual.
Jika Anda tidak memerlukan post
asosiasi sama sekali, Anda kurang beruntung ketika menggunakan FetchType.EAGER
karena tidak ada cara untuk menghindari mengambilnya. Itu sebabnya lebih baik digunakanFetchType.LAZY
secara default.
Tetapi, jika Anda ingin menggunakan post
asosiasi, maka Anda dapat menggunakan JOIN FETCH
untuk merinci masalah permintaan N + 1:
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
join fetch pc.post p
""", PostComment.class)
.getResultList();
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Kali ini, Hibernate akan menjalankan satu pernyataan SQL:
SELECT
pc.id as id1_1_0_,
pc.post_id as post_id3_1_0_,
pc.review as review2_1_0_,
p.id as id1_0_1_,
p.title as title2_0_1_
FROM
post_comment pc
INNER JOIN
post p ON pc.post_id = p.id
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
Untuk detail lebih lanjut tentang mengapa Anda harus menghindari FetchType.EAGER
strategi pengambilan, lihat artikel ini juga.
FetchType.LAZY
Bahkan jika Anda beralih menggunakan FetchType.LAZY
secara eksplisit untuk semua asosiasi, Anda masih bisa menabrak masalah N +1.
Kali ini, post
asosiasi dipetakan seperti ini:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
Sekarang, ketika Anda mengambil PostComment
entitas:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Hibernate akan menjalankan pernyataan SQL tunggal:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
Tetapi, jika sesudahnya, Anda akan merujuk post
asosiasi yang malas :
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Anda akan mendapatkan masalah permintaan N + 1:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
Karena post
asosiasi diambil malas, pernyataan SQL sekunder akan dieksekusi ketika mengakses asosiasi malas untuk membangun pesan log.
Sekali lagi, perbaikan terdiri dalam menambahkan JOIN FETCH
klausa ke permintaan JPQL:
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
join fetch pc.post p
""", PostComment.class)
.getResultList();
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Dan, seperti dalam FetchType.EAGER
contoh ini, permintaan JPQL ini akan menghasilkan pernyataan SQL tunggal.
Bahkan jika Anda menggunakan FetchType.LAZY
dan tidak mereferensikan asosiasi anak dari @OneToOne
hubungan JPA dua arah , Anda masih dapat memicu masalah kueri N +1.
Untuk detail lebih lanjut tentang bagaimana Anda bisa mengatasi masalah kueri N + 1 yang dihasilkan oleh @OneToOne
asosiasi, lihat artikel ini .
Cara mendeteksi secara otomatis masalah kueri N + 1
Jika Anda ingin secara otomatis mendeteksi masalah kueri N + 1 di lapisan akses data Anda, artikel ini menjelaskan bagaimana Anda bisa melakukannya dengan menggunakan proyek db-util
open-source.
Pertama, Anda perlu menambahkan ketergantungan Maven berikut:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId>
<version>${db-util.version}</version>
</dependency>
Setelah itu, Anda hanya perlu menggunakan SQLStatementCountValidator
utilitas untuk menegaskan pernyataan SQL yang mendasari yang dihasilkan:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
Jika Anda menggunakan FetchType.EAGER
dan menjalankan test case di atas, Anda akan mendapatkan kegagalan test case berikut:
SELECT
pc.id as id1_1_,
pc.post_id as post_id3_1_,
pc.review as review2_1_
FROM
post_comment pc
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2
-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!
Untuk detail lebih lanjut tentang proyek db-util
open-source, lihat artikel ini .