Статьи

Проблемы с производительностью сопоставленных коллекций Hibernate

Прежде всего, эта статья была вдохновлена ​​после презентации Берта Беквита о Advanced GORM — производительности, настройке и мониторинге в SpringOne 2GX 27 января 2011 года . Короче говоря, Берт Беквит (Burt Beckwith) обсуждает потенциальные проблемы с производительностью, используя сопоставленные коллекции и кэш-память второго уровня Hibernate в GORM, а также стратегии, позволяющие избежать таких потерь производительности.

Тем не менее проблемы с производительностью отображаемых коллекций, которые Берт Беквит указывает в своей презентации, относятся ко всем приложениям с поддержкой Hibernate в целом. Вот почему после просмотра его презентации я понял, что он предлагает именно то, что я сам делал, и продиктовал моим коллегам при разработке с использованием сопоставленных коллекций в Hibernate .

Ниже приведены 5 вещей, которые следует учитывать при работе с отображенными коллекциями Hibernate :

Давайте рассмотрим следующий классический пример «Библиотека — Визит»:

В следующем классе Library есть коллекция экземпляров Visit :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package eg;
import java.util.Set;
 
public class Library {
    private long id;
    private Set visits;
 
    public long getId() { return id; }
    private void setId(long id) { this.id=id; }
 
    private Set getVisits() { return visits; }
    private void setVisits(Set visits) { this.visits=visits; }
 
    ....
    ....
}

Ниже приводится класс Визит :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package eg;
import java.util.Set;
 
public class Visit {
    private long id;
    private String personName;
 
    public long getId() { return id; }
    private void setId(long id) { this.id=id; }
 
    private String getPersonName() { return personName; }
    private void setPersonName(String personName) { this.personName=personName; }
 
    ....
    ....
}

Предполагая, что библиотека имеет несколько уникальных посещений и что каждое посещение соотносится с отдельной библиотекой, можно использовать однонаправленную связь один-ко-многим, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<hibernate-mapping>
 
    <class name="Library">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <set name="visits">
            <key column="library_id" not-null="true"/>
            <one-to-many class="Visit"/>
        </set>
    </class>
 
    <class name="Visit">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <property name="personName"/>
    </class>
 
</hibernate-mapping>

Я также приведу пример определений таблиц для схемы, описанной выше:

1
2
3
4
5
6
create table library (id bigint not null primary key )
create table visit(id bigint not null
                     primary key,
                     personName varchar(255),
                     library_id bigint not null)
alter table visit add constraint visitfk0 (library_id) references library

Так что не так с этой картиной?

Возможные узкие места производительности возникнут, когда вы попытаетесь добавить в сопоставленную коллекцию . Как видите, коллекция реализована в виде набора . Наборы гарантируют уникальность среди содержащихся в них элементов. Так как же Hibernate узнает, что новый предмет уникален, чтобы добавить его в набор ? Ну не удивляйся; добавление в набор требует загрузки всех доступных предметов из базы данных. Hibernate сравнивает каждого из них с новым, чтобы гарантировать уникальность. Более того, вышеприведенное является стандартным поведением, которое мы не можем обойти, даже если знаем из-за бизнес-правил, что новый элемент уникален!

Использование реализации List для нашей сопоставленной коллекции также не решит проблему узкого места в производительности при добавлении в нее элементов. Хотя списки не гарантируют уникальность, они гарантируют заказ товара. Поэтому для поддержания правильного порядка элементов в нашем отображаемом списке Hibernate должен извлечь всю коллекцию, даже если мы добавляем в конец списка.

На мой взгляд, длинный путь — это просто добавить одно новое посещение библиотеки , вы согласны?

Кроме того, приведенный выше пример хорошо работает в разработке, где у нас всего несколько посещений. В производственных средах, где каждая библиотека может иметь миллионы посещений, просто представьте себе снижение производительности при попытке добавить еще одну!

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

Когда вы удаляете / добавляете объект из / в коллекцию, номер версии владельца коллекции увеличивается. Таким образом, существует высокий риск искусственных исключений оптимистической блокировки на объекте библиотеки при одновременном создании посещений . Мы характеризуем исключения для оптимистической блокировки как «искусственные», потому что они происходят с объектом владельца коллекции ( библиотекой ), который, по нашему мнению, мы не редактируем (но мы делаем!), Когда мы добавляем / удаляем элемент из коллекции Visits .

Позвольте мне указать, что те же правила применяются к типу связи « многие ко многим ».

Так в чем же решение?

Решение простое: удалите сопоставленную коллекцию из объекта владельца ( библиотеки ) и выполните вставку и удаление элементов « Визит » «вручную». Предлагаемое решение влияет на использование следующими способами:

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

Чтобы сохранить вещи в чистоте и порядке, вы можете восстановить псевдо-коллекцию «посещений» обратно в объект « Библиотека », реализовав вспомогательный метод, который будет запрашивать базу данных и возвращать все объекты « Визит», связанные с определенной библиотекой . Кроме того, вы можете реализовать несколько вспомогательных методов, которые будут выполнять фактическую вставку и удаление записей посещений в пункте « Посещение» .

Ниже мы представляем обновленные версии класса Library, класса Visit и отображения Hibernate для соответствия предлагаемому решению:

Сначала обновленный класс Библиотеки :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package eg;
import java.util.Set;
 
public class Library {
    private long id;
 
    public long getId() { return id; }
    private void setId(long id) { this.id=id; }
 
    public Set getVisits() {
      // TODO : return select * from visit where visit.library_id=this.id
    }
    ....
    ....
}

Как вы можете видеть, мы удалили сопоставленную коллекцию и ввели метод « getVisits () », который должен использоваться для возврата всех элементов Visit для конкретного экземпляра библиотеки (комментарий TODO находится в псевдокоде).

Ниже приводится обновленный класс Посетить :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package eg;
import java.util.Set;
 
public class Visit {
    private long id;
    private String personName;
    private long library_id;
 
    public long getId() { return id; }
    private void setId(long id) { this.id=id; }
 
    private String getPersonName() { return personName; }
    private void setPersonName(String personName) { this.personName=personName; }
 
    private long getLibrary_id() { return library_id; }
    private void setLibrary_id(long library_id) { this. library_id =library_id; }
 
    ....
    ....
}

Как вы можете видеть, мы добавили поле « library_id » к объекту « Визит», чтобы иметь возможность соотнести его с элементом библиотеки .

Последнее обновление карты Hibernate :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<hibernate-mapping>
 
    <class name="Library">
        <id name="id">
            <generator class="sequence"/>
        </id>
    </class>
 
    <class name="Visit">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <property name="personName"/>
        <property name="library_id"/>
    </class>
 
</hibernate-mapping>

Итак, никогда не использовать сопоставленные коллекции в Hibernate ?

Ну, если честно, нет. Вы должны изучить каждый случай, чтобы решить, что делать. Стандартный подход хорош, если коллекции достаточно малы — обе стороны в случае схемы объединения многих со многими . Кроме того, коллекции будут содержать прокси, поэтому они меньше, чем реальные экземпляры, пока не будут инициализированы.

Удачного кодирования! Не забудьте поделиться!

Джастин

PS

После относительно продолжительного обсуждения этой статьи на TheServerSide один из наших читателей, Эб Бра (Eb Bras), предоставил полезный список «советов и хитростей» Hibernate, и посмотрим, что он скажет:

Вот некоторые из моих советов и приемов Hibernate, которые я задокументировал долгий путь:

обратная =»истина»
Используйте это в максимально возможной степени в связи родитель-потомок один-ко-многим (с другим объектом или типом значения, который используется в качестве объекта).
Это свойство устанавливается для тега коллекции как «set» и означает, что многие-к-одному владеют ассоциацией и отвечают за все вставки / обновления / удаления БД. Это делает ассоциацию частью ребенка.
Это сохранит обновление базы данных для внешнего ключа, поскольку это произойдет непосредственно при вставке дочернего элемента.

Особенно при использовании «set» в качестве типа отображения, он может повысить производительность, так как дочерний элемент не нужно добавлять в родительскую коллекцию, что может сохранить загрузку всей коллекции. То есть: из-за природы отображения набора вся коллекция должна всегда загружаться при добавлении нового дочернего элемента, поскольку это единственный способ, которым hibernate может гарантировать, что новая запись не является дубликатом, который является функцией набора JRE интерфейс.
В случае если это касается коллекции компонентов (= коллекция, содержащая только чистые типы значений), обратный = true игнорируется и не имеет смысла, так как Hibernate полностью контролирует объекты и выберет лучший способ выполнить свои грубые действия.
Если это касается отсоединенных объектов DTO (не содержащих каких-либо объектов гибернации), hibernate удалит все дочерние объекты типа значения, а затем вставит их, поскольку не знает, какой объект является новым или существующим, поскольку он был полностью отсоединен. Hibernate относится к нему как к новой коллекции.

ленивый Set.getChilds () это зло
Будьте осторожны, используя getChilds (), который возвращает Set и будет лениво загружать все дочерние элементы.
Не используйте это, когда вы хотите добавить или удалить только ребенка, как это будет первым

всегда использовать равно / хэш-код
Убедитесь, что вы всегда используете equals / hashcode для каждого объекта, которым управляет Hibernate, даже если это не кажется важным. Это также относится к объектам типа Value.
Если объект не содержит свойств, которые являются кандидатами на использование хеш-кода equals / hashcode, используйте суррогатный ключ, который состоит, например, из UUID.
Hibernate использует equals / hashcode, чтобы выяснить, присутствует ли объект в БД. Если это касается существующего объекта, но Hibernate считает, что это новый объект, потому что equals / hashcode не реализован правильно, Hibernate выполнит вставку и, возможно, удалит старое значение.
Особенно это важно для типов значений в Set, которые необходимо протестировать, поскольку они экономят трафик в БД.
Идея: вы даете Hibernate больше знаний, которые он может использовать для оптимизации своих действий.

использование версии
Всегда используйте свойство version с сущностью или типом значения, который используется в качестве сущности.
Это приводит к уменьшению трафика в дБ, поскольку Hibernate использует эту информацию, чтобы определить, касается ли он нового или существующего объекта. Если это свойство отсутствует, ему придется нажать на базу данных, чтобы узнать, касается ли оно нового или существующего объекта.

нетерпеливый выбор
Не ленивые коллекции (дочерние) по умолчанию загружаются с помощью дополнительного запроса выбора, который выполняется только после загрузки родителя из БД.
Дочерние элементы могут быть загружены в том же запросе, что и загрузка родительского элемента, путем активизации выборки, которая выполняется путем установки атрибута «fetch = join» в теге отображения коллекции. Если включено, дочерние элементы загружаются через левое внешнее соединение.
Проверьте, улучшает ли это производительность. Если происходит много соединений или если это касается таблицы с множеством столбцов, производительность будет ухудшаться, а не улучшаться.

использовать суррогатный ключ в типе значения дочерний объект
Hibernate создаст первичный ключ в дочернем типе значения отношения родитель-потомок, который состоит из всех ненулевых столбцов. Это может привести к странным комбинациям первичных ключей, особенно когда используется столбец даты. Столбец даты не должен быть частью первичного ключа, так как его миллисекундная часть приведет к тому, что первичные ключи почти никогда не будут одинаковыми. Это приводит к странным и, вероятно, плохим показателям производительности БД.
Чтобы улучшить это, мы используем суррогатный ключ во всех дочерних объектах типа значения, который является единственным ненулевым свойством. Затем Hibernate создаст первичный ключ, который состоит из внешнего ключа и суррогатного ключа, который является логическим и хорошо работает.
Обратите внимание, что суррогатный ключ используется только для оптимизации базы данных, и его не обязательно использовать в хэш-коде equals /, который должен состоять из бизнес-логики, если это возможно.

Статьи по Теме :