Рубрики
Без рубрики

Лучший способ отобразить отношения JPA и Hibernate во многих отношениях

Узнайте, как лучше всего сопоставить множество ассоциаций при использовании JPA и гибернации, чтобы выполняемые инструкции SQL были эффективными.

Автор оригинала: Vlad Mihalcea.

Вступление

В этой статье я покажу вам лучший способ сопоставить множество ассоциаций при использовании JPA и Hibernate.

Какими бы простыми ни были аннотации JPA, не всегда очевидно, насколько они эффективны за кулисами. В этой статье я собираюсь показать вам, как лучше всего использовать JPA @ManyToMany аннотация при использовании режима гибернации.

Модель предметной области

Предполагая, что у нас есть следующие таблицы базы данных:

Типичная связь базы данных “многие ко многим” включает две родительские таблицы, которые связаны через третью, содержащую два внешних ключа, ссылающихся на родительские таблицы.

Реализация ассоциации JPA и гибернации ManyToMany с использованием списка

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

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    public Post() {}

    public Post(String title) {
        this.title = title;
    }

    @ManyToMany(cascade = { 
        CascadeType.PERSIST, 
        CascadeType.MERGE
    })
    @JoinTable(name = "post_tag",
        joinColumns = @JoinColumn(name = "post_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private List tags = new ArrayList<>();

    //Getters and setters ommitted for brevity

    public void addTag(Tag tag) {
        tags.add(tag);
        tag.getPosts().add(this);
    }

    public void removeTag(Tag tag) {
        tags.remove(tag);
        tag.getPosts().remove(this);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Post)) return false;
        return id != null && id.equals(((Post) o).getId());
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

@Entity(name = "Tag")
@Table(name = "tag")
public class Tag {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

    @ManyToMany(mappedBy = "tags")
    private List posts = new ArrayList<>();

    public Tag() {}

    public Tag(String name) {
        this.name = name;
    }

    //Getters and setters ommitted for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Tag tag = (Tag) o;
        return Objects.equals(name, tag.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
} 

В вышеупомянутом отображении следует отметить несколько аспектов, которые стоит объяснить/

Во-первых, ассоциация теги в Записи сущности определяет только СОХРАНЕНИЕ и СЛИЯНИЕ каскадные типы. Как поясняется в этой статье , УДАЛИТЬ переход состояния сущности не имеет никакого смысла для ассоциации @ManyToMany JPA, поскольку это может вызвать удаление цепочки, которое в конечном итоге уничтожит обе стороны ассоциации.

Как объясняется в этой статье , методы утилиты добавления/удаления являются обязательными, если вы используете двунаправленные ассоциации, чтобы убедиться, что обе стороны связи синхронизированы.

Сущность Post использует идентификатор сущности для равенства, поскольку в ней отсутствует какой-либо уникальный бизнес-ключ. Как объяснено в этой статье , вы можете использовать идентификатор сущности для обеспечения равенства, если убедитесь, что он остается согласованным во всех переходах состояния сущности .

Объект Tag имеет уникальный бизнес-ключ, который помечен аннотацией, специфичной для гибернации @NaturalID . В этом случае уникальный бизнес-ключ является лучшим кандидатом для проверки на равенство .

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

Для получения более подробной информации об аннотации @Natural ознакомьтесь с этой статьей .

Хотя сопоставление является правильным с точки зрения JPA и гибернации, с точки зрения базы данных предыдущее сопоставление многих отношений вообще неэффективно. Чтобы понять, почему это так, вам необходимо зарегистрировать и проанализировать автоматически сгенерированные инструкции SQL .

Учитывая, что у нас есть следующие организации:

final Long postId = doInJPA(entityManager -> {
    Post post1 = new Post("JPA with Hibernate");
    Post post2 = new Post("Native Hibernate");

    Tag tag1 = new Tag("Java");
    Tag tag2 = new Tag("Hibernate");

    post1.addTag(tag1);
    post1.addTag(tag2);

    post2.addTag(tag1);

    entityManager.persist(post1);
    entityManager.persist(post2);

    return post1.id;
});

При удалении Тега сущности из Сообщения :

doInJPA(entityManager -> {
    Tag tag1 = new Tag("Java");
    Post post1 = entityManager.find(Post.class, postId);
    post1.removeTag(tag1);
});

Hibernate генерирует следующие инструкции SQL:

SELECT p.id AS id1_0_0_,
       t.id AS id1_2_1_,
       p.title AS title2_0_0_,
       t.name AS name2_2_1_,
       pt.post_id AS post_id1_1_0__,
       pt.tag_id AS tag_id2_1_0__
FROM   post p
INNER JOIN 
       post_tag pt 
ON     p.id = pt.post_id
INNER JOIN 
       tag t 
ON     pt.tag_id = t.id
WHERE  p.id = 1

DELETE FROM post_tag
WHERE  post_id = 1

INSERT INTO post_tag
       ( post_id, tag_id )
VALUES ( 1, 3 )

Итак, вместо удаления только одного post_tag запись , Hibernate удаляет все post_tag строки, связанные с заданным post_id , и затем повторно вставляет оставшиеся. Это совсем не эффективно, потому что это дополнительная работа для базы данных, особенно для воссоздания индексов, связанных с базовыми внешними ключами.

По этой причине не рекомендуется использовать java.util. Список для @ManyToMany Ассоциаций JPA.

Реализация ассоциации JPA и гибернации ManyToMany с использованием набора

Вместо Списка , мы можем использовать Набор .

Сообщение сущность теги ассоциация будет изменена следующим образом:

@ManyToMany(cascade = { 
    CascadeType.PERSIST, 
    CascadeType.MERGE
})
@JoinTable(name = "post_tag",
    joinColumns = @JoinColumn(name = "post_id"),
    inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set tags = new HashSet<>();

И объект Tag подвергнется той же модификации:

@ManyToMany(mappedBy = "tags")
private Set posts = new HashSet<>();

Если вы беспокоитесь об отсутствии предопределенного порядка ввода, то вам нужно использовать SortedSet вместо Set при предоставлении либо @SortNatural или @SortComparator .

Например, если объект Tag реализует Сопоставимый , , вы можете использовать аннотацию @Shortnatural

@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "post_tag",
    joinColumns = @JoinColumn(name = "post_id"),
    inverseJoinColumns = @JoinColumn(name = "tag_id")
)
@SortNatural
private SortedSet tags = new TreeSet<>();

Теперь при повторном запуске предыдущего тестового набора Hibernate генерирует следующие инструкции SQL:

SELECT p.id AS id1_0_0_,
       t.id AS id1_2_1_,
       p.title AS title2_0_0_,
       t.name AS name2_2_1_,
       pt.post_id AS post_id1_1_0__,
       pt.tag_id AS tag_id2_1_0__
FROM   post p
INNER JOIN 
       post_tag pt 
ON     p.id = pt.post_id
INNER JOIN 
       tag t 
ON     pt.tag_id = t.id
WHERE  p.id = 1

DELETE FROM post_tag
WHERE  post_id = 1 AND tag_id = 3

Намного лучше! Выполняется только одна инструкция DELETE, которая удаляет связанную запись post_tag .

Вывод

Использование JPA и Hibernate очень удобно, так как это может повысить производительность разработчиков. Однако это не означает, что вы должны жертвовать производительностью приложения.

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

Поэтому при использовании аннотации @ManyToMany всегда используйте java.util. Установите и избегайте java.util. Список .