Как я могу сделать отношение JPA OneToOne ленивым
В этом приложении, которое мы разрабатываем, мы заметили, что просмотр был особенно медленным. Я профилировал представление и заметил, что hibernate выполнил один запрос, который занял 10 секунд, даже если в базе данных было всего два объекта для извлечения. Все отношения OneToMany
и ManyToMany
были ленивыми, так что это не было проблемой. При проверке фактического выполняемого SQL я заметил, что в запросе было более 80 соединений.
При дальнейшем изучении проблемы я заметил, что проблема была вызвана глубокой иерархией OneToOne
и ManyToOne
отношений между классами сущностей. Итак, я подумал, я просто сделаю их выборку ленивой, это должно решить проблему. Но аннотирование либо @OneToOne(fetch=FetchType.LAZY)
либо @ManyToOne(fetch=FetchType.LAZY)
, похоже, не работает. Либо я получаю исключение, либо тогда они на самом деле не заменяются прокси-объектом и, следовательно, являются ленивыми.
Есть идеи, как я заставлю это работать? Обратите внимание, что я не использую persistence.xml
для определения отношений или деталей конфигурации, все делается в коде Java.
Переведено автоматически
Ответ 1
Прежде всего, некоторые пояснения к ответу KLE:
Неограниченная (обнуляемая) взаимно однозначная связь - единственная, которая не может быть проксирована без инструментария байт-кода. Причина этого в том, что объект owner ДОЛЖЕН знать, должно ли свойство association содержать прокси-объект или NULL, и он не может определить это, просмотрев столбцы своей базовой таблицы из-за того, что взаимно однозначное сопоставление обычно выполняется через shared PK, поэтому его в любом случае приходится быстро извлекать, что делает прокси бессмысленным. Вот более подробное объяснение.
ассоциации "многие к одному" (и, очевидно, "один ко многим") не страдают от этой проблемы. Объект-владелец может легко проверить свой собственный FK (и в случае "один ко многим" изначально создается пустой прокси-сервер коллекции и заполняется по запросу), поэтому ассоциация может быть ленивой.
Замена отношения "один к одному" на "один ко многим" в значительной степени никогда не является хорошей идеей. Вы можете заменить его уникальным отношением "многие к одному", но есть другие (возможно, лучшие) варианты.
У Роба Х. есть допустимая точка зрения, однако вы можете быть не в состоянии реализовать ее в зависимости от вашей модели (например, если ваша взаимно однозначная связь обнуляема).
Теперь, что касается первоначального вопроса:
A) @ManyToOne(fetch=FetchType.LAZY)
должно работать просто отлично. Вы уверены, что оно не перезаписывается в самом запросе? Можно указать join fetch
в HQL и / или явно установить режим выборки через Criteria API, который будет иметь приоритет над аннотацией класса. Если это не так и у вас все еще возникают проблемы, пожалуйста, опубликуйте свои классы, запрос и результирующий SQL для более подробного обсуждения.
B) @OneToOne
сложнее. Если оно определенно не обнуляемо, воспользуйтесь предложением Роба Х. и укажите его как таковое:
@OneToOne(optional = false, fetch = FetchType.LAZY)
В противном случае, если вы можете изменить свою базу данных (добавить столбец внешнего ключа в таблицу owner), сделайте это и сопоставьте ее как "присоединенную":
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()
и в Other Entity:
@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()
Если вы не можете этого сделать (и не можете смириться с быстрой выборкой), инструментирование байт-кода - ваш единственный вариант. Однако я должен согласиться с CPerkins - если у вас есть 80 !!! объединения из-за нетерпеливых ассоциаций OneToOne, у вас проблемы посерьезнее, чем это :-)
Ответ 2
Чтобы заставить отложенную загрузку работать с обнуляемыми взаимно однозначными сопоставлениями, вам нужно разрешить hibernate выполнять инструментирование во время компиляции и добавить @LazyToOne(value = LazyToOneOption.NO_PROXY)
к взаимно однозначному отношению.
Пример сопоставления:
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()
Пример расширения файла сборки Ant (для выполнения инструментария гибернации во время компиляции):
<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>
Ответ 3
Если вы не используете улучшение байт-кода, вы не сможете лениво извлекать родительскую @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
ассоциацией на родительской стороне.
Ответ 4
Вот что у меня работает (без инструментария):
Вместо использования @OneToOne
с обеих сторон, я использую @OneToMany
в обратной части отношения (с mappedBy
). Это превращает свойство в коллекцию (List
в примере ниже), но я перевожу его в элемент в получателе, делая его прозрачным для клиентов.
Эта настройка работает лениво, то есть выбор выполняется только при вызове getPrevious()
or 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); }
}