Как использовать аннотации для определения разных типов отношений в Hibernate 4 и Spring?

У меня есть два класса: Foo и Bar, как показано ниже:

public class Foo {
     private Long fooId;

     private Bar bar;
     //Yes, this doesn't actually make any sense,
     //having both a list and a single object here, its an example.
     private List<Bar> bars;
}

public class Bar {
    private Long barId;

    private Foo foo;
}

Как реализовать (однонаправленные/двунаправленные) отношения "один ко многим", "многие-к-одному" или "многие ко многим", используя аннотации для Hibernate 4 для этих классов?

Также как мне настроить мой один-ко-многим для удаления сирот, ленивую загрузку и что вызывает LazyInitialiaizationException при работе с коллекциями и как решить проблему?

Ответы

Ответ 1

Создание отношений с аннотациями

Предположим, что все классы, аннотированные с помощью @Entity и @Table

Однонаправленные отношения "один к одному"

public class Foo{
    private UUID fooId;

    @OneToOne
    private Bar bar;
}

public class Bar{
    private UUID barId;
    //No corresponding mapping to Foo.class
}

Двунаправленные отношения "один к одному", управляемые Foo.class

public class Foo{
    private UUID fooId;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "barId")
    private Bar bar;
}

public class Bar{
    private UUID barId;

    @OneToOne(mappedBy = "bar")
    private Foo foo;
}

Uni-Directional One to Many Отношения с помощью таблицы соединений, управляемой пользователем

public class Foo{
    private UUID fooId;

    @OneToMany
    @JoinTable(name="FOO_BAR",
        joinColumns = @JoinColumn(name="fooId"),
        inverseJoinColumns = @JoinColumn(name="barId"))
    private List<Bar> bars;
}

public class Bar{
    private UUID barId;

    //No Mapping specified here.
}

@Entity
@Table(name="FOO_BAR")
public class FooBar{
    private UUID fooBarId;

    @ManyToOne
    @JoinColumn(name = "fooId")
    private Foo foo;

    @ManyToOne
    @JoinColumn(name = "barId")
    private Bar bar;

    //You can store other objects/fields on this table here.
}

Очень часто используется Spring Безопасность при настройке объекта User, у которого есть список Role, который они могут выполнять. Вы можете добавлять и удалять роли для пользователя, не беспокоясь о том, что каскады удаляют Role.

Двунаправленное отношение "один ко многим" с использованием сопоставления внешних ключей

public class Foo{
    private UUID fooId;

    @OneToMany(mappedBy = "bar")
    private List<Bar> bars;
}

public class Bar{
    private UUID barId;

    @ManyToOne
    @JoinColumn(name = "fooId")
    private Foo foo;
}

Двунаправленные от многих до многих с помощью таблицы объединенного соединения Hibernate

public class Foo{
    private UUID fooId;

    @OneToMany
    @JoinTable(name="FOO_BAR",
        joinColumns = @JoinColumn(name="fooId"),
        inverseJoinColumns = @JoinColumn(name="barId"))
    private List<Bar> bars;
}

public class Bar{
    private UUID barId;

    @OneToMany
    @JoinTable(name="FOO_BAR",
        joinColumns = @JoinColumn(name="barId"),
        inverseJoinColumns = @JoinColumn(name="fooId"))
    private List<Foo> foos;
}

Двунаправленные Многие для многих, используя пользовательский элемент таблицы соединений

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

public class Foo{
    private UUID fooId;

    @OneToMany(mappedBy = "bar")
    private List<FooBar> bars;
}

public class Bar{
    private UUID barId;

    @OneToMany(mappedBy = "foo")
    private List<FooBar> foos;
}

@Entity
@Table(name="FOO_BAR")
public class FooBar{
    private UUID fooBarId;

    @ManyToOne
    @JoinColumn(name = "fooId")
    private Foo foo;

    @ManyToOne
    @JoinColumn(name = "barId")
    private Bar bar;

    //You can store other objects/fields on this table here.
}

Определение, какая сторона двунаправленного отношения "владеет" соотношением:

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

Пример: объект User имеет список Roles, объявленный на нем. В большинстве приложений система будет обрабатывать экземпляры объекта User чаще, чем экземпляры объекта Roles. Следовательно, я бы сделал объект Role принадлежащей стороне отношения и манипулировал объектами Role через список Role на User каскадом. Для практического примера см. Пример двунаправленного от одного до большого. Как правило, вы будете каскадировать все изменения в этом сценарии, если у вас нет конкретных требований делать иначе.

Определение типа fetchType

Лентабельные коллекции привели к большему количеству проблем с SO, чем мне хотелось бы посмотреть, потому что по умолчанию Hibernate будет загружать связанные объекты лениво. Не имеет значения, является ли отношение одним-к-одному или многими-ко-многим в соответствии с документами Hibernate:

По умолчанию Hibernate использует ленивый выбор для наборов и ленивый выбор прокси для однозначных ассоциаций. Эти значения по умолчанию имеют смысл для большинства ассоциаций в большинстве приложений.

Рассмотрим эти два цента о том, когда следует использовать fetchType.LAZY vs fetchType.EAGER на ваших объектах. Если вы знаете, что в 50% случаев вам не потребуется доступ к коллекции на родительском объекте, я бы использовал fetchType.LAZY.

Преимущества производительности в этом огромны и растут только по мере того, как вы добавляете больше объектов в свою коллекцию. Это связано с тем, что для загруженной коллекции Hibernate выполняет тонну проверки за кулисами, чтобы гарантировать, что ни одна из ваших данных не устарела. Хотя я рекомендую использовать Hibernate для коллекций, имейте в виду, что для fetchType.EAGER существует ограничение производительности **. Однако возьмите наш пример объекта Person. Довольно вероятно, что при загрузке Person мы захотим узнать, что они выполняют Roles. Обычно я отмечу эту коллекцию как fetchType.EAGER. НЕ РЕФЛЕКСИВНО ОТМЕТЬТЕ СВОЕ СОБРАНИЕ КАК fetchType.EAGER ПРОСТО ПОЛУЧИТЬ ВОКРУГ A LazyInitializationException. Не только это плохо по соображениям производительности, это обычно указывает на то, что у вас есть проблема с дизайном. Спросите себя, должна ли эта коллекция действительно быть загруженной коллекцией, или я делаю это только для доступа к коллекции в этом методе. У Hibernate есть способы обойти это, что не сильно влияет на производительность ваших операций. Вы можете использовать следующий код на вашем уровне Service, если вы хотите инициализировать ленивую загрузку только для этого вызова.

//Service Class
@Override
@Transactional
public Person getPersonWithRoles(UUID personId){
    Person person = personDAO.find(personId);

    Hibernate.initialize(person.getRoles());

    return person;
}

Вызов Hibernate.initialize вызывает создание и загрузку объекта коллекции. Однако будьте осторожны, если вы передадите только экземпляр Person, вы получите прокси-сервер своего Person обратно. Дополнительную информацию см. В документации. Единственным недостатком этого метода является то, что у вас нет контроля над тем, как Hibernate действительно будет получать вашу коллекцию объектов. Если вы хотите контролировать это, вы можете сделать это в своем DAO.

//DAO
@Override
public Person findPersonWithRoles(UUID personId){
    Criteria criteria = sessionFactory.getCurrentSession().createCritiera(Person.class);

    criteria.add(Restrictions.idEq(personId);
    criteria.setFetchMode("roles", FetchMode.SUBSELECT);
}

Производительность здесь зависит от того, что вы указываете FetchMode. Я прочитал ответыкоторые говорят, чтобы использовать FetchMode.SUBSELECT по соображениям производительности. Связанный ответ более подробно рассматривается, если вы действительно заинтересованы.

Если вы хотите прочитать меня, как я повторяю, не стесняйтесь проверить мой другой ответ здесь

Определение каскадного направления

Спящий режим может выполнять каскадные операции либо в обоих направлениях, либо в двух направлениях. Поэтому, если у вас есть список Role на User, вы можете каскадировать изменения в Role в обоих направлениях. Если вы измените имя конкретного Role на User, спящий режим может автоматически обновить связанный Role в таблице Role.

Однако это не всегда желаемое поведение. Если вы думаете об этом, в этом случае внесение изменений в Role на основе изменений в User не имеет никакого смысла. Однако имеет смысл идти в противоположном направлении. Измените имя Role на самом объекте Role, и это изменение может быть каскадным для всех объектов User, которые имеют на нем Role.

С точки зрения эффективности имеет смысл создавать/обновлять объекты Role, сохраняя объект User, к которому они принадлежат. Это означает, что вы отмечаете аннотацию @OneToMany как каскадную. Я приведу пример:

public User saveOrUpdate(User user){
    getCurrentSession.saveOrUpdate(user);
    return user;
}

В приведенном выше примере Hibernate сгенерирует запрос INSERT для объекта User, а затем каскадирует создание Role после того, как User был вставлен в базу данных. Затем эти вставные операторы смогут использовать PK User в качестве своего внешнего ключа, поэтому в итоге вы получите инструкции для вставки N + 1, где N - количество объектов Role в списке пользователей.

И наоборот, если вы хотите сохранить отдельные объекты Role, каскадирующие обратно к объекту User, можно сделать:

//Assume that user has no roles in the list, but has been saved to the
//database at a cost of 1 insert.
public void saveOrUpdateRoles(User user, List<Roles> listOfRoles){
    for(Role role : listOfRoles){
        role.setUser(user);
        getCurrentSession.saveOrUpdate(role);
    }
}

Это приводит к вставкам N + 1, где N - число Role в listOfRoles, но также N операторов обновлений, сгенерированных, поскольку Hibernate каскадирует добавление каждой Role в таблицу User. Этот метод DAO имеет более высокую временную сложность, чем наш предыдущий метод O (n), в отличие от O (1), потому что вам нужно перебирать список ролей. Избегайте этого, если это вообще возможно.

На практике, однако, обычно собственная сторона отношений будет там, где вы отмечаете свои каскады, и вы обычно будете каскадировать все.

Удаление сирот

Hibernate может работать для вас, если вы удалите все ассоциации с объектом. Предположим, у вас есть User, у которого есть список Role, и в этом списке есть ссылки на 5 разных ролей. Допустим, вы удалили Role с именем ROLE_EXAMPLE, и случается, что ROLE_EXAMPLE не существует ни в одном другом объекте User. Если у вас есть orphanRemoval = true, установленный в анноте @OneToMany, Hibernate удалит текущий объект "осиротевший" из базы данных каскадом.

Удаление сирот не должно быть включено в каждом случае. Фактически, наличие orphanRemoval в нашем примере выше не имеет смысла. Просто потому, что no User может выполнять любое действие, которое представляет объект ROLE_EXAMPLE, это не означает, что любое будущее User никогда не сможет выполнить действие.

Этот Q & A предназначен для дополнения официальной документации Hibernate, которая имеет большую конфигурацию XML для этих отношений.

Эти примеры не предназначены для копирования в производственный код. Они являются типичными примерами создания и управления различными объектами и их отношениями с помощью JPA-аннотаций для настройки Hibernate 4 в рамках Spring Framework. В примерах предполагается, что все классы имеют поля ID, объявленные в следующем формате: fooId. Тип этого поля идентификатора не имеет значения.

** Недавно нам пришлось отказаться от использования Hibernate для задания на вставку, где мы вставляли в базу данных через 80 000+ объектов через коллекцию. Hibernate съел всю память кучи, проверив сборку и разбив систему.

<суб > ПРЕДУПРЕЖДЕНИЕ: Я НЕ ЗНАЮ, ЕСЛИ ЭТИ ПРИМЕРЫ БУДУТ РАБОТАТЬ С СТАНДАРТНЫМ ЛИНИЕМ

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

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