Как я могу сделать отношения JPA OneToOne ленивыми

212

В этом приложении, которое мы разрабатываем, мы заметили, что просмотр был особенно медленным. Я профилировал представление и заметил, что hibernate выполнил один запрос, который занял 10 секунд, даже если в базе данных было только два объекта для выборки. Все OneToManyи ManyToManyотношения были ленивыми, так что это не проблема. При проверке фактического выполнения SQL я заметил, что в запросе было более 80 соединений.

Далее, изучив проблему, я заметил, что проблема была вызвана глубокой иерархией OneToOneи ManyToOneотношениями между классами сущностей. Итак, я думал, я просто сделаю их ленивыми, что должно решить проблему. Но аннотирование @OneToOne(fetch=FetchType.LAZY)или @ManyToOne(fetch=FetchType.LAZY)не работает. Либо я получаю исключение, либо они фактически не заменяются прокси-объектом и поэтому ленивы.

Есть идеи, как мне заставить это работать? Обратите внимание, что я не использую persistence.xmlдля определения отношений или деталей конфигурации, все делается в коде Java.

Ким Л
источник

Ответы:

218

Прежде всего, некоторые пояснения к ответу KLE :

  1. Неограниченная (обнуляемая) взаимно-однозначная ассоциация является единственной, которую нельзя проксировать без инструментирования байт-кодом. Причина этого заключается в том, что объект-владелец ДОЛЖЕН знать, должно ли свойство ассоциации содержать прокси-объект или NULL, и он не может определить это, просматривая столбцы своей базовой таблицы из-за того, что один-к-одному обычно сопоставляют через общий PK, поэтому в любом случае должен быть извлечен с нетерпением, делая прокси бессмысленным. Вот более подробное объяснение.

  2. ассоциации многие-к-одному (и, очевидно, один-ко-многим) не страдают от этой проблемы. Субъект-владелец может легко проверить свой собственный FK (и в случае «один ко многим» пустой прокси-сервер коллекции создается изначально и заполняется по требованию), поэтому сопоставление может быть ленивым.

  3. Замена один-к-одному на один-ко-многим никогда не бывает хорошей идеей. Вы можете заменить его уникальным много-к-одному, но есть и другие (возможно, лучшие) варианты.

Роб Х. имеет действительную точку, однако , вы не можете быть в состоянии осуществить это в зависимости от модели (например , если ваш один-к-одному ассоциации является обнуляемым).

Теперь, что касается оригинального вопроса:

А) @ManyToOne(fetch=FetchType.LAZY)должно работать просто отлично. Вы уверены, что он не перезаписывается в самом запросе? Можно указать join fetchв HQL и / или явно установить режим выборки через Criteria API, который будет иметь приоритет над аннотацией класса. Если это не так, и у вас все еще есть проблемы, пожалуйста, опубликуйте ваши классы, запрос и полученный SQL для более подробного обсуждения.

Б) @OneToOneхитрее. Если это определенно не обнуляемо, воспользуйтесь предложением Роба Х. и укажите его так:

@OneToOne(optional = false, fetch = FetchType.LAZY)

В противном случае, если вы можете изменить свою базу данных (добавить столбец внешнего ключа в таблицу владельцев), сделайте это и сопоставьте ее как «присоединенную»:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

и в OtherEntity:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

Если вы не можете этого сделать (и не можете жить с нетерпеливым извлечением), байт-код - ваш единственный выбор. Я должен согласиться с CPerkins , однако - если у вас есть 80 !!! присоединяется из-за активных ассоциаций OneToOne, у вас есть большие проблемы, чем это :-)

ChssPly76
источник
Может быть, есть другой вариант, но я лично не проверял его: на неограниченной стороне, используйте a one-to-oneс формулой, как select other_entity.id from other_entity where id = other_entity.id. Конечно, это не идеально для выполнения запросов.
Фредерик
1
опционально = ложь, у меня не работает. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", необязательно = false) private FundSeriesDetailEntity fundSeriesDetail;
Олег Куц
21

Чтобы ленивая загрузка работала с взаимно однозначными сопоставлениями, вам нужно позволить hibernate делать инструментарий времени компиляции и добавлять отношение @LazyToOne(value = LazyToOneOption.NO_PROXY)«один к одному».

Пример сопоставления:

@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

Пример расширения файла Ant Build (для инструментария Hibernate во время компиляции):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>
Kdeveloper
источник
3
Почему LazyToOneOption.NO_PROXYи нет LazyToOneOption.PROXY?
Тельмо Маркиз
Это не отвечает на вопрос «почему», но этот факт также подтверждается и здесь (ближе к концу раздела «Типичное отображение»): vladmihalcea.com/…
DanielM
12

Основная идея XToOnes в Hibernate заключается в том, что в большинстве случаев они не ленивы.

Одна из причин заключается в том, что, когда Hibernate должен решить поставить прокси-сервер (с идентификатором) или ноль,
он все равно должен заглянуть в другую таблицу. чтобы присоединиться. Стоимость доступа к другой таблице в базе данных значительна, поэтому она может также извлечь данные для этой таблицы в этот момент (не ленивое поведение) вместо того, чтобы извлекать их в более позднем запросе, который потребует второго доступа к та же таблица.

Отредактировано: подробности см. В ответе ChssPly76 . Этот менее точный и подробный, ему нечего предложить. Спасибо ChssPly76.

KLE
источник
Здесь есть несколько неправильных вещей - ниже я предоставил другой ответ с объяснением (слишком много материала, он не помещается в комментарии)
ChssPly76
8

Вот кое-что, что работает для меня (без инструментов):

Вместо того, чтобы использовать @OneToOneс обеих сторон, я использую @OneToManyв обратной части отношения (ту, с которой mappedBy). Это делает свойство коллекцией ( Listв примере ниже), но я перевожу его в элемент в получателе, делая его прозрачным для клиентов.

Эта настройка работает лениво, то есть выбор производится только при вызове getPrevious()или getNext()вызове - и только один выбор для каждого вызова.

Структура таблицы:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

Класс:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}
acdcjunior
источник
7

Как я объяснил в этой статье , если вы не используете улучшение байт-кода , вы не можете лениво извлекать родительскую сторону@OneToOne ассоциацию .

Однако, чаще всего вам даже не нужна родительская ассоциация, если вы используете @MapsIdна стороне клиента:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

С @MapsId, тоid свойство в дочерней таблице служит как Первичным ключом, так и Внешним ключом для родительской таблицы Первичный ключ.

Итак, если у вас есть ссылка на родительский Postобъект, вы можете легко получить дочерний объект, используя идентификатор родительского объекта:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

Таким образом, у вас не будет проблем с N + 1 запросами, которые могут быть вызваны mappedBy @OneToOneсвязью на родительской стороне.

Влад Михалча
источник
таким образом, мы больше не можем каскадировать операции от родителя к ребенку: /
Hamdi
Для persist это просто дополнительный вызов persist, для delete вы можете использовать каскад DDL.
Влад Михалча
6

В нативных XML-сопоставлениях Hibernate этого можно добиться, объявив сопоставление « один к одному» с установленным для true ограниченным атрибутом. Я не уверен, что такое эквивалент аннотации Hibernate / JPA, и быстрый поиск документа не дал ответа, но, надеюсь, это даст вам возможность продолжить.

Роб Х
источник
5
+1 за хорошее предложение; к сожалению, это не всегда применимо, поскольку модель предметной области может фактически требовать обнуляемости. Правильный способ отобразить это с помощью аннотаций@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76
Я попробовал это и не увидел улучшения производительности. Я все еще видел много запросов в выводе из спящего режима через отладчик.
P.Brian.Mackey
3

Как уже прекрасно объясняется ChssPly76, проксите в Hibernate не помогают (безусловным обнуляемым) ассоциацию один-к-одному, НО есть трюк объяснил здесь , чтобы избежать , чтобы настроить аппаратуру. Идея состоит в том, чтобы обмануть Hibernate, так как класс сущностей, который мы хотим использовать, уже инструментирован: вы вручную вводите его в исходный код. Это просто! Я реализовал его с CGLib в качестве поставщика байт-кода, и он работает (убедитесь, что вы настроили lazy = "no-proxy" и fetch = "select", а не "join", в вашем HBM).

Я думаю, что это хорошая альтернатива реальным (я имею в виду, автоматическим) инструментальным средствам, когда у вас есть только одно однозначное отношение, которое вы хотите сделать ленивым. Основным недостатком является то, что решение зависит от поставщика байт-кода, который вы используете, поэтому прокомментируйте свой класс точно, потому что в будущем вам, возможно, придется сменить поставщика байт-кода; Конечно, вы также модифицируете свой компонент по технической причине, и это не хорошо.

Pino
источник
1

Этот вопрос довольно старый, но в Hibernate 5.1.10 есть новое, более удобное решение.

Ленивая загрузка работает за исключением родительской стороны ассоциации @OneToOne. Это связано с тем, что в Hibernate нет другого способа узнать, назначать ли этой переменной нуль или прокси. Более подробную информацию вы можете найти в этой статье

  • Вы можете активировать ленивую загрузку улучшения байт-кода
  • Или вы можете просто удалить родительскую сторону и использовать клиентскую сторону с @MapsId, как описано в статье выше. Таким образом, вы обнаружите, что родительская сторона вам на самом деле не нужна, поскольку дочерний элемент имеет один и тот же идентификатор с родительским, поэтому вы можете легко получить дочерний элемент, зная идентификатор родительского элемента.
Туй
источник
0

Если отношение не должно быть двунаправленным, то @ElementCollection может быть проще, чем использование ленивой коллекции One2Many.

Стефан
источник