Як я можу зробити відношення JPA OneToOne ледачим


212

У цій програмі, яку ми розробляємо, ми помітили, що погляд був дуже повільним. Я профілював погляд і помітив, що був один запит, виконаний сплячим режимом, який зайняв 10 секунд, навіть якщо в базі даних було лише два об'єкти. Все OneToManyі ManyToManyвідносини були ліниві , так що не було проблемою. Перевіряючи фактичне виконання SQL, я помітив, що у запиті було понад 80 приєднань.

Далі вивчаючи це питання, я помітив, що проблема була викликана глибокою ієрархією OneToOneта ManyToOneвідносинами між класами сутностей. Отже, я подумав, я просто зроблю їх ледачими, що повинно вирішити проблему. Але примітка @OneToOne(fetch=FetchType.LAZY)або @ManyToOne(fetch=FetchType.LAZY), здається, не працює. Або я отримую виняток, або тоді вони насправді не замінюються проксі-об'єктом і, таким чином, лінуються.

Будь-які ідеї, як я змушу це працювати? Зауважте, що я не використовую persistence.xmlдля визначення відносин або деталей конфігурації, все робиться в коді Java.

Відповіді:


218

По-перше, кілька роз'яснень відповіді KLE :

  1. Нестримне (зведене) асоціація один на один - це єдине, яке не може бути проксифіковано без інструментів байт-коду. Причиною цього є те, що суб'єкт власника ПОВИНЕН знати, чи має властивість асоціації містити проксі-об'єкт або NULL, і він не може визначити, дивлячись на стовпці базової таблиці через те, що один-на-один зазвичай відображається через спільний ПК, так що все одно має бути нетерплячим, що робить проксі безглуздим. Ось більш детальне пояснення.

  2. багато асоціацій (і, очевидно, один) багатьом не страждають від цього питання. Організація-власник може легко перевірити свій власний ФК (а у випадку, коли їх багато хто, спочатку проксі-сервер колекції створюється та заповнюється на вимогу), тому асоціація може бути лінивою.

  3. Заміна одного на одного на одного - багатьом - це майже ніколи не є хорошою ідеєю. Ви можете замінити його на унікальні багато в одному, але є й інші (можливо, кращі) варіанти.

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

Тепер, що стосується оригінального питання:

А) @ManyToOne(fetch=FetchType.LAZY)повинен працювати просто чудово. Ви впевнені, що це не перезаписано в самому запиті? Можливо вказати join fetchв HQL та / або явно встановити режим отримання за допомогою 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, у вас є більші проблеми, ніж це :-)


Можливо, є й інший варіант, але я особисто його не перевіряв: з не обмеженого боку використовуйте one-to-oneформулу типу select other_entity.id from other_entity where id = other_entity.id. Звичайно, це не ідеально для виконання запитів.
Фредерік

1
необов'язково = помилково, не працює для мене. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", необов'язковий = помилковий) приватний FundSeriesDetailEntity fundSeriesDetail;
Олег Куць

21

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

Приклад картографування:

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

Приклад розширення файлу Ant 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>

3
Чому LazyToOneOption.NO_PROXYі ні LazyToOneOption.PROXY?
Тельмо Маркес

Це не відповідає "чому", але цей факт також стверджується і тут (наприкінці розділу "Типове картографування"): vladmihalcea.com/…
DanielM

12

Основна ідея XToOnes в режимі глибокого сну полягає в тому, що вони в більшості випадків не лінуються.

Однією з причин є те, що коли Hibernate має вирішити поставити проксі (з ідентифікатором) або null,
він все одно повинен заглянути в іншу таблицю, щоб приєднатися. Вартість доступу до іншої таблиці в базі даних є значною, тому в цей момент вона може також отримати дані для цієї таблиці (не лінива поведінка), а не витягувати їх у наступному запиті, який потребував би другого доступу до та сама таблиця.

Відредаговано: для детальної інформації зверніться до відповіді ChssPly76 . Цей менш точний і детальний, він не може нічого запропонувати. Спасибі ChssPly76.


Тут є кілька помилок - я надав ще одну відповідь нижче з поясненням (занадто багато матеріалів, не впишуться в коментар)
ChssPly76,

8

Ось щось, що працює для мене (без інструментарію):

Замість того, щоб використовувати @OneToOneз обох сторін, я використовую @OneToManyв оберненій частині відносин (той, що має mappedBy). Це робить власність колекцією ( Listу прикладі нижче), але я перекладаю її на предмет у програмі getter, роблячи її прозорою для клієнтів.

Ця установка працює ліниво, тобто вибір здійснюється лише тоді, коли 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); }

}

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асоціацією на батьківській стороні.


таким чином ми більше не можемо робити каскадні операції від батька до дитини: /
Хамді,

Для збереження це просто додатковий виклик, для видалення ви можете використовувати каскад DDL.
Влад Михальча

6

У вітчизняних відображеннях Xber Hibernate XML ви можете досягти цього, оголосивши зіставлення один на один із обмеженим атрибутом, встановленим на істинне. Я не впевнений, що таке еквівалент анотації Hibernate / JPA, і швидкий пошук документа не дав відповіді, але, сподіваємось, це може призвести до продовження.


5
+1 за гарну пропозицію; на жаль, це не завжди застосовно, оскільки модель домену може насправді вимагати зворотності. Правильний спосіб відобразити це за допомогою анотацій@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76,

Я спробував це і не побачив покращення продуктивності. Я все ще бачив багато запитів у сплячому виході через налагоджувач.
P.Brian.Mackey

3

Як вже прекрасно пояснюється ChssPly76, проксі в Hibernate не допомагають (безумовним обнуляє) асоціацію один-до-одного, АЛЕ є трюк пояснив тут , щоб уникнути , щоб налаштувати апаратуру. Ідея полягає в тому, щоб обдурити сплячий факт, що клас сутності, який ми хочемо використовувати, вже зафіксований: ви інструментуєте його вручну у вихідному коді. Це легко! Я реалізував його разом із CGLib як постачальника байт-коду, і він працює (переконайтеся, що у своєму HBM налаштовано lazy = "no-proxy" та fetch = "select", а не "приєднатися").

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


1

Це питання досить старе, але в режимі Hibernate 5.1.10 є нове краще комфортне рішення.

Ледаче завантаження працює за винятком батьківської сторони асоціації @OneToOne. Це тому, що в режимі Hibernate немає іншого способу дізнатися, чи призначити нуль чи проксі цій змінній. Більш детально ви можете ознайомитися в цій статті

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

0

Якщо відношення не повинно бути двонаправленим, то @ElementCollection може бути простішим, ніж використання лінивої колекції One2Many.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.