درک و حل مشکل پرس و جو N+1 در JPA

در N+1 مشکل پرس و جو هنگامی که یک پرس و جو اولیه (1) توسط N نمایش داده شود – یکی برای هر ردیف نتیجه از اولین پرس و جو. اگرچه این ممکن است برای مجموعه داده های کوچک مسئله ای نباشد ، اما می تواند منجر به تخریب جدی عملکرد در مقیاس شود.
چرا در JPA اتفاق می افتد
مشکل N+1 JPA ارتباط نزدیکی با نحوه روابط موجودیت با استفاده از JPQL دارد ، که یک انتزاع شی گرا بر روی SQL است. نمایش داده های JPQL معمولاً فقط موجودیت ریشه را هدف قرار می دهد و نهادهای مرتبط را نادیده بگیرید مگر اینکه صریحاً به دست بیایدبشر
حتی روش های پرس و جو Data بهار JPA و querydsl در نهایت JPQL را در زیر کاپوت اجرا می کنند ، بنابراین درک این رفتار ضروری است.
هنگامی که یک نهاد مرتبط در هنگام دسترسی به نهاد مورد نیاز است (بر اساس FetchType
) ، Hibernate اجرا می کند نمایش داده شد برای بازیابی آنها بیایید ببینیم که این در عمل چگونه کار می کند.
مثال کد: N+1 در عمل
تعاریف موجودیت
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Transport {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "driver_id")
private Driver driver;
public static Transport from(Driver driver) {
return new Transport(null, driver);
}
}
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Driver {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public static Driver from(String name) {
return new Driver(null, name);
}
}
کد آزمون برای مشاهده رفتار n+1
@SpringBootTest
@Transactional
class TransportRepositoryTest {
@Autowired
private TransportRepository transportRepository;
@Autowired
private DriverRepository driverRepository;
@Autowired
private EntityManager entityManager;
@Test
void testNPlusOneQueryProblem() {
int driverCount = 3;
for (int i = 0; i < driverCount; i++) {
Driver driver = Driver.from("Driver " + i);
driverRepository.save(driver);
}
List<Driver> allDrivers = driverRepository.findAll();
for (Driver driver : allDrivers) {
transportRepository.save(Transport.from(driver));
}
entityManager.flush();
entityManager.clear();
System.out.println("=== Finding all transports ===");
List<Transport> allTransports = transportRepository.findAll();
System.out.println("=== Accessing driver properties (will trigger N+1 queries) ===");
for (Transport transport : allTransports) {
System.out.println("Transport ID: " + transport.getId() + ", Driver Name: " + transport.getDriver().getName());
}
}
}
چرا ما شفاف و پاک می شویم؟
ما صریحاً تماس می گیریم flush()
وت clear()
در EntityManager
برای جلوگیری از بازگشت متن (حافظه نهان سطح اول) در حال بازگشت اشخاص در حال حاضر در حافظه. این تضمین می کند که تمام نمایش داده ها به پایگاه داده ارسال می شوند و ما می توانیم رفتار N+1 را به طور دقیق رعایت کنیم.
خروجی کنسول (ساده شده)
=== Finding all transports ===
select t1_0.id, t1_0.driver_id from transport t1_0
=== Accessing driver properties (will trigger N+1 queries) ===
select d1_0.id, d1_0.name from driver d1_0 where d1_0.id=?
select d1_0.id, d1_0.name from driver d1_0 where d1_0.id=?
select d1_0.id, d1_0.name from driver d1_0 where d1_0.id=?
اگر تغییر کنیم چه اتفاقی می افتد FetchType.EAGER
؟
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "driver_id")
private Driver driver;
با بارگذاری مشتاق ، ممکن است انتظار داشته باشد که JPA هم حمل و نقل و هم راننده را در یک پرس و جو واحد بارگذاری کند. اما در واقعیت ، نتیجه اینگونه به نظر می رسد:
=== Finding all transports ===
select t1_0.id, t1_0.driver_id from transport t1_0
select d1_0.id, d1_0.name from driver d1_0 where d1_0.id=?
select d1_0.id, d1_0.name from driver d1_0 where d1_0.id=?
select d1_0.id, d1_0.name from driver d1_0 where d1_0.id=?
JPA هنوز یک پرس و جو را برای هر یک از مرتبط اجرا می کند Driver
بشر چرا؟
JPQL انجام می دهد نه به طور خودکار به اشخاص مرتبط بپیوندید ، حتی با تنظیمات Fetch Eager. مگر در مواردی که صریحاً مشخص شده باشد ، نهادهای مرتبط هنوز به طور جداگانه پرس و جو می شوند.
با هر دو اتفاق می افتد LAZY
وت EAGER
-
بارگذاری تنبل: با
FetchType.LAZY
، JPA Proxiesdriver
مرجع هنگام دسترسی (به عنوان مثال ،transport.getDriver().getName()
) ، خواب زمستانی آن را در صورت تقاضا واگذار می کند. -
بارگذاری ماهر: با
FetchType.EAGER
، JPA ابتدا نهاد والدین را بارگیری می کند ، سپس نمایش داده های جداگانه را برای اشخاص مرتبط اجرا می کند – به ویژه هنگامی که JPQL بدون پیوستن استفاده می شود.
حتی روشهای مخزن JPA پیش فرض مانند findAll()
یا findById()
از SQL تولید شده توسط Hibernate استفاده کنید و هنگام پیمایش در روابط ، همچنان N+1 را ایجاد می کند.
راه حل برای مشکل N+1
از JPQL استفاده کنید fetch join
@Query("SELECT t FROM Transport t JOIN FETCH t.driver")
List<Transport> findAllWithDriver();
این به JPA دستور می دهد تا همراهان را واگذار کند Driver
در یک پرس و جو واحد ، از بارگذاری تنبل در کل خودداری کنید.
از querydsl استفاده کنید fetchJoin()
public List<Transport> findAllWithDriverUsingQuerydsl() {
QTransport transport = QTransport.transport;
QDriver driver = QDriver.driver;
return queryFactory
.selectFrom(transport)
.leftJoin(transport.driver, driver).fetchJoin()
.fetch();
}
این رویکرد از API مسلط Querydsl برای صریح دستور Hibernate برای انجام یک عضویت در بین Transport
و مرتبط با آن Driver
بشر
استفاده کردن @EntityGraph
@EntityGraph(attributePaths = "driver")
List findAll();
این به همان نتیجه می رسد fetch join
اما با نام های روش JPA داده های بهار پاک می شود.
نقش نسل Hibernate SQL
Hibernate بسته به مسیر دسترسی ممکن است بین استراتژی های مختلف واکشی انتخاب کند:
-
EntityManager.find()
ممکن است منجر به پیوستن به نمایش داده شود. - JPQL نوع واکشی را نادیده می گیرد مگر اینکه پیوستن به صریح اعلام شود.
-
FetchType.EAGER
همیشه به عنوان پیوستن مورد تقدیر قرار نمی گیرد.
شما عملکرد پرس و جو خود را کنترل می کنید صریح بودن در مورد استراتژی واکشی شما
خلاصه
- n + 1 = 1 پرس و جو اولیه + n نمایش داده شدگان فردی در هر نهاد
- بدون در نظر گرفتن
FetchType
- JPQL انجمن ها را نادیده می گیرد مگر اینکه
JOIN FETCH
استفاده می شود - حل شده با استفاده از پیوندهای Fetch ، نمودارهای موجودی یا querydsl Fetch
- همیشه با سیاهههای پرس و جو واقعی اعتبار کنید
درک N+1 فقط یک تئوری نیست – این چیزی است که می توانید آزمایش کنید و مشاهده کنید. مسلح با استراتژی مناسب ، می توانید آن را از بین ببرید و برنامه JPA خود را واقعاً درجه تولید کنید.