четверг, 6 декабря 2007 г.

Заметки о Hibernate. Атрибут inverse

Hibernate - один из самых распространенных средств для использования модели ORM в java-приложениях. Но у него есть один существенный недостаток - довольно мало документации, где бы описывались именно тонкости применения Hibernate в тех или иных случаях. Я уж молчу про то, что на русском языке документации практически нет.

Данной публикацией я открываю серию статей о данной ORM системе. Я здесь не собираюсь цитировать книги про хибернейт, ORM и т.д., а буду делиться своими наблюдениями, результатами экспериментов и т.д. Дело в том, что по роду деятельности мне приходится чуть ли не каждый день с ним работать и кое-какой опыт имеется.

Очень важными для меня будут Ваши коментарии, указания на ошибки и неточности, вопросы и предложения.

Первая статья будет про такой интересный, многими используемый но не всегда понятный параметр как inverse.


Практически в любом приложении, использующем Hibernate возникает необходимость использования двунаправленых ассоциаций. Двунаправленная ассоциация очень удобна, когда обоим классам в ней учавствующим необходимо знать друг о друге. Например сущность-автопроизводитель содержит коллекцию моделей произведенных авто, а сущность-авто содержит поле "производитель". Рассмотрим двунаправленную асоциацию на примере отношения "many-to-one" и соответственно "one-to-many".

Пусть есть сущность "документ" (DMSDocument) и "рецензия на документ" (DMSReview). Соответственно документ имеет коллекцию рецензий, а рецензия - поле, указывающее к какому документу она относится.

Файл мэппинга:

<?xml version="1.0" encoding="UTF-8" ?>



<!DOCTYPE hibernate-mapping SYSTEM

       "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">



<hibernate-mapping package="ru.naumen.dms.bobjects.dmsdocument">

    <class name="DMSDocument" table="TBL_DMS_DOCUMENT">

        <key column="Document_UUID"

            foreign-key="FK_DMSCoreBO_DMSDocument" />




        <property name="htmlContent"

            type="ru.naumen.core.util.BinaryBlobType" />




        <set name="reviewSet" inverse="false">

            <key column="document" />

            <one-to-many class="ru.naumen.dms.bobjects.dmsdocument.DMSReview" />

        </set>



    </class>



    <class name="DMSReview" table="TBL_DMS_REVIEW">

        <key column="Review_UUID"

            foreign-key="FK_DMSCoreBO_DMSReview" />




        <property name="htmlContent"

            type="ru.naumen.core.util.BinaryBlobType" />




                <many-to-one name="document" column="document"

            class="ru.naumen.dms.bobjects.dmsdocument.DMSDocument"

            not-null="true" />


    </class>

</hibernate-mapping>


Разберем логику добавления нового документа при inverse = "false".

Пусть за добавление документа отвечает следующий код:

// Загружаем документ из БД

DMSDocument document = (DMSDocument)     PrefixObjectLoaderFacade.getObjectByUUID("coreboo2k04pg0000hd7rmt2hil696q4");



DMSReview review = new DMSReview();

review.setTitle("Name");

review.setHtmlContent("Test".getBytes("UTF8"));



// Важная для демонстрации логики inverse строчка          

document.addReview(review);



// Непосредственно добавление                    

Transaction tx = HibernateUtil.currentSession().beginTransaction();

HibernateUtil.currentSession().save(review);

tx.commit();


При inverse=false у нас будут выполнены следующие SQL-запросы (посмотреть можно установив опцию hibernate.show_sql в true):

Hibernate:

  INSERT

  INTO

      TBL_DMS_REVIEW

      (TITLE, htmlContent, document, UUID)

  VALUES

      (?, ?, ?, ?)



Hibernate:

  UPDATE

      TBL_BO

  SET

      version=?,

      Title=?,

      creationDate=?,

      removed=?,

      removalDate=?,

      ParentId=?

  WHERE

      UUID=?

      AND version=?



Hibernate:

  UPDATE

      TBL_DMS_REVIEW

  SET

      document=?

  WHERE

      UUID=?



Вот отсюда начинается самое интересное - алгоритм добавления сущностей в БД.

1. Сначала сохраняется сущность DMSReview. Именно здесь нас подстерегает основная опастность: хорошо если соответствующая полю document запись в БД уже есть. А если записи нет, то в поле document будет записан NULL, но создавая файл-мэппинга мы указали ограничение not-null="true", соответсвенно при генерации DDL (Data Definition Language) по файлу мэппинга будет создано ограничение целостности (поле document не может принимать значение NULL), произойдет ошибка и выполнится откат транзакции.

2. Так как мы добавили следующий код:

// Важная для демонстрации логики inverse строчка          

document.addReview(review);


то происходит обновление записи о документе. Хибернейт не отслеживает какие поля менялись и менялись ли вообще (в частности, приведенная выше строчка вообще не меняет ни одно поле в таблице tbl_dms_document). Он "видит", что объет изменился и просто делает его апдейт. Соответственно в случае, если объект DMSDocument не изменяется в рамках транзакции, то и данного запроса не будет.

3. Происходит изменение записи о рецензии. Ей наконец-то устанавливается нужное значение поля document - начинает работать ассоциация.

Теперь рассмотрим противоположную ситуацию: inverse="true".

Сначала рассмотрим, что происходит в данном случае. А происходит ни больше ни меньше как следующее:

Hibernate:

  INSERT

  INTO

      TBL_DMS_REVIEW

      (TITLE, htmlContent, document, UUID)

  VALUES

      (?, ?, ?, ?)



Hibernate:

  UPDATE

      TBL_BO

  SET

      version=?,

      Title=?,

      creationDate=?,

      removed=?,

      removalDate=?,

      ParentId=?

  WHERE

      UUID=?

      AND version=?



 


Как видим ситуация изменилась - пропал последний апдейт! Дело в том, что ассоциация теперь как бы инвертирована - она управляется не со стороны DMSDocument, а со стороны DMSReview! Соответственно, DMSReview уже на этапе первого insert "знает" к какому документу она относится и поле document сразу заполнено корректно. Поэтому надобность в последнем апдейте отпадает.

Если же объект DMSDocument не существует, а создается вместе с коллекцией объектов DMSReview, то сначала в базу будет записан он, соответственно у него появится id и объекты DMSReview будут опять таки сохранены с корректным значением поля document, естественно равным значению поля id сущности DMSDocument. Нарушения целостности в случае inverse="true" не будет.

Понравилось сообщение - подпишись на блог через RSS

6 комментариев:

samsonych комментирует...

Наглядно и хорошо оформленно. Классная статья.
Павел, а подскажи чем ты highlighting посветку для кода генерируешь?

Samolisov Pavel комментирует...

Большое спасибо за комментарий. Рад что статья оказалась полезной.

По поводу подцветки кода. Я использую quickhighlighter.com/. Он генерируем html-код, который можно скопировать из специального поля ввода. Из плюсов могу отметить - получается очень красиво, есть даже ссылки на javadoc-по библиотечным классам (например если нажать на слово "String"). Из минусов - необходимо внедрять css-код для выбранных языков в разметку блога. Соответственно т.к. получаемых html заточен на определенный css-код то корректно подцветка отображается только на страницах блога. В RSS уже не корректно. НО! Можно в quickhighlighter.com поставить галочку "Combine Style and HTML Code" и все стили будут внедрятся непосредственно в HTML. Кстати планирую опробовать эту фичу. Посмотрю как будет код смотреться в Яндекс-ленте.

samsonych комментирует...

Благодарствую за ссылку и описание сервиса. Думаю вполне потянет на отдельный пост, ибо для программеров-блоггеров очень полезная штукенция.
Обязательно воспользуюсь сервисом, а то как-то совсем по другому статья выглядит когда код с подцветкой.

Samolisov Pavel комментирует...

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

Вообще на мысль заюзать подцветку меня навело мое первое сообщение с кодом на php. Блоггер не разрешает использовать символы < и >, а вспоминать таблицу html-сущностей не хотелось. Кстати еще один + в копилку сервиса - забываешь о сущностях. Если использовать подцветчики на базе JavaScript - так не получится.

budanoff комментирует...

Я вот очень плотно столкнулся с inverse="false"

Есть сущности - "язык" и "пользователь"
Каждый пользователь может
1) знать язык
2) учить язык
выходит, у меня двойная связь многие-ко-многим.
Итого, с учетом перехода к связям один-ко-многим получается 4 таблицы.


Гинерил маппиги в NetBeans по готовой базе.
Маппинг по юзеру:





















но нетбинс, изначально, создал один set с inverse="false" а другой с inverse="true"
в итоге у меня при создании пользователя добавлялись только те языки которые он знал.

Влад комментирует...

Спасибо за статью, пригодилась.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!