برنامه نویسی

درک و حل مشکل پرس و جو 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 Proxies driver مرجع هنگام دسترسی (به عنوان مثال ، 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 خود را واقعاً درجه تولید کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا