Статьи

Паттерны реализации JPA: двунаправленные ассоциации и ленивая загрузка

В этом блоге предполагается, что вы знакомы с примером Order / OrderLine, который я представил в первых двух блогах этой серии. Если нет, просмотрите пример.

Рассмотрим следующий код:

OrderLine orderLineToRemove = orderLineDao.findById(30);
orderLineToRemove.setOrder(null);

Целью этого кода является отсоединение OrderLine с Order, с которым он был ранее связан. Вы можете представить это до удаления объекта OrderLine (хотя вы также можете использовать аннотацию @PreRemove, чтобы сделать это автоматически) или когда вы хотите присоединить OrderLine к другому объекту Order.

Если вы запустите этот код, вы обнаружите, что будут загружены следующие объекты:

  1. OrderLine с идентификатором 30.
  2. Заказ, связанный с OrderLine. Это происходит потому, что метод OrderLine.setOrder вызывает метод Order.internalRemoveOrderLine для удаления OrderLine из его родительского объекта Order.
  3. Все остальные строки заказа, связанные с этим заказом! Набор Order.orderLines загружается, когда из него удаляется объект OrderLine с идентификатором 30.


В зависимости от поставщика JPA, это может занять два-три запроса. Hibernate использует три запроса; по одному для каждой из строк, упомянутых здесь. OpenJPA нужно только два запроса, потому что он загружает Order с OrderLine, используя внешнее соединение.

Однако интересно то, что все сущности OrderLine загружены. Если есть много OrderLines, это может быть очень дорогой операцией. Поскольку это происходит, даже если вы не заинтересованы в фактическом содержании этой коллекции, вы платите высокую цену за сохранение двунаправленных ассоциаций нетронутыми.

До сих пор я обнаружил три различных решения этой проблемы:

  1. Не делайте ассоциацию двунаправленной; сохранить только ссылку от дочернего объекта на родительский объект. В этом случае это будет означать удаление orderLines, установленного в объекте Order. Чтобы получить OrderLines, которые идут с Order, вы должны вызвать метод findOrderLinesByOrder для OrderLineDao . Так как это приведет к извлечению всех дочерних объектов, и мы столкнулись с этой проблемой, потому что их много, вам нужно будет написать более конкретные запросы, чтобы найти нужное подмножество дочерних объектов. Недостатком этого подхода является то, что он означает, что Order не может получить доступ к своим OrderLines без необходимости проходить через уровень обслуживания (проблема, о которой мы поговорим в более позднем блоге) или передавая их вызывающим методом.
  2. Используйте специфичную для Hibernate аннотацию @LazyCollection, чтобы коллекция загружалась « очень лениво », например:
    @OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
    @org.hibernate.annotations.LazyCollection(org.hibernate.annotations.LazyCollectionOption.EXTRA)
    public Set<OrderLine> orderLines = new HashSet<OrderLine>();

    Эта функция должна позволить Hibernate работать с очень большими коллекциями. Например, когда вы запрашиваете размер набора, Hibernate не загружает все элементы коллекций. Вместо этого он выполнит запрос SELECT COUNT (*) FROM …. Но что еще интереснее: изменения в коллекции ставятся в очередь, а не применяются напрямую. Если при доступе к коллекции ожидаются какие-либо изменения, сеанс сбрасывается перед выполнением дальнейшей работы.

    Хотя это работает нормально для метода size (), оно не работает, когда вы пытаетесь перебрать элементы набора (см. Выпуск JIRA HHH-2087, который был открыт в течение двух с половиной лет). У дополнительной ленивой загрузки размера также есть по крайней мере две открытых ошибки: HHH-1491 и HHH-3319 . Все это заставляет меня поверить в то, что дополнительная функция отложенной загрузки в Hibernate — хорошая идея, но не полностью зрелая (пока?).

  3. Вдохновленный механизмом Hibernate для откладывания операций над коллекцией до тех пор, пока вы действительно не нуждаетесь в их выполнении, я изменил класс Order, чтобы сделать нечто подобное. Сначала очередь операций была добавлена ​​в качестве переходного поля в класс Order:
    private transient Queue<Runnable> queuedOperations = new LinkedList<Runnable>();

    Затем методы internalAddOrderLine и internalRemoveOrderLine были изменены, чтобы они напрямую не изменяли набор orderLines. Вместо этого они создают экземпляр соответствующего подкласса класса QueuedOperation. Этот экземпляр инициализируется с объектом OrderLine для добавления или удаления, а затем помещается в очередь queuesOperations:

    public void internalAddOrderLine(final OrderLine line) {
    queuedOperations.offer(new Runnable() {
    public void run() { orderLines.add(line); }
    });
    }

    public void internalRemoveOrderLine(final OrderLine line) {
    queuedOperations.offer(new Runnable() {
    public void run() { orderLines.remove(line); }
    });
    }

    Finally the getOrderLines method is changed so that it executes any queued operations before returning the set:

    public Set<? extends OrderLine> getOrderLines() {
    executeQueuedOperations();
    return Collections.unmodifiableSet(orderLines);
    }

    private void executeQueuedOperations() {
    for (;;) {
    Runnable op = queuedOperations.poll();
    if (op == null)
    break;
    op.run();
    }
    }

    If there were more methods that need the set to be fully up to date, they would invoke the executeQueuedOperations method in a similar manner.

    The downside here is that your domain objects get cluttered with even more «link management code» than we already had managing bidirectional associations. Abstracting out this logic to a separate class is left as an exercise for the reader. ;-)

Of course this problem not only occurs when you have bidirectional associations. It surfaces any time you are manipulating large collections mapped with @OneToMany or @ManyToMany. Bidirectional associations just makes the cause less obvious because you think you are only manipulating a single entity.

You should not use the method described above at bullet #3 if you are cascading any operations to the mapped collection. If you postpone modifications to the collections your JPA provider won’t know about the added or removed elements and will cascade operations to the wrong entities. This means that any entities added to a collection on which you have set @CascadeType.PERSIST won’t be persisted unless you explicitly invoke EntityManager.persist on them. On a similar note the Hibernate specific @org.hibernate.annotations.CascadeType.DELETE_ORPHAN annotation will only remove orphaned child entities when they are actually removed from Hibernate’s PersistentCollection.

In any case, now you know what causes this performance hit and three possible ways to solve it. I am interested to hear whether you ran into this problem and how you solved it.

From http://blog.xebia.com