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

Как синхронизировать двунаправленные ассоциации сущностей с JPA и гибернацией

Узнайте, как синхронизировать двунаправленные ассоциации сущностей @OneToMany, @OneToOne и @ManyToMany при использовании JPA (API сохранения Java) и гибернации.

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

Отвечая на этот вопрос StackOverflow , я понял, что неплохо бы обобщить, как должны синхронизироваться различные двунаправленные ассоциации при использовании JPA и Hibernate.

Поэтому в этой статье вы узнаете, как и почему вы всегда должны синхронизировать обе стороны отношений сущностей, независимо от того, является ли это @OneToMany , @OneToOne или @ManyToMany .

Давайте предположим, что у нас есть родительская Запись сущность, которая имеет двунаправленную связь с Комментарием к записи дочерней сущностью:

Сущность Комментарий к сообщению выглядит следующим образом:

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    @GeneratedValue
    private Long id;

    private String review;

    @ManyToOne(
        fetch = FetchType.LAZY
    )
    @JoinColumn(name = "post_id")
    private Post post;

    //Getters and setters omitted for brevity

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

Есть несколько вещей, которые следует отметить в Комментарии к сообщению сопоставлении сущностей выше.

Во-первых, ассоциация @ManyToOne использует Тип выборки.ЛЕНИВАЯ стратегия, потому что по умолчанию @ManyToOne и @OneToOne ассоциации используют Тип выборки.НЕТЕРПЕЛИВАЯ стратегия, которая плохо влияет на производительность .

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

Объект Post отображается следующим образом:

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

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(
        mappedBy = "post", 
        cascade = CascadeType.ALL, 
        orphanRemoval = true
    )
    private List comments = new ArrayList<>();

    //Getters and setters omitted for brevity

    public void addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
    }

    public void removeComment(PostComment comment) {
        comments.remove(comment);
        comment.setPost(null);
    }
}

комментарии @OneToMany ассоциация помечена атрибутом mappedBy , который указывает, что сторона @ManyToOne отвечает за обработку этой двунаправленной ассоциации.

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

По этой причине сущность Post определяет методы синхронизации добавления комментария и удаления комментария состояния сущности.

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

Post post = new Post();
post.setTitle("High-Performance Java Persistence");

PostComment comment = new PostComment();
comment.setReview("JPA and Hibernate");
post.addComment(comment);

entityManager.persist(post);

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

Post post = entityManager.find(Post.class, 1L);
PostComment comment = post.getComments().get(0);

post.removeComment(comment);

Для получения более подробной информации о том, как лучше всего сопоставить @OneToMany ассоциацию, ознакомьтесь с этой статьей .

Для связи один к одному предположим, что родительская Запись сущность имеет Сведения о записи дочернюю сущность, как показано на следующей диаграмме:

Дочерняя Информация о публикации сущность выглядит следующим образом:

@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;
    
    //Getters and setters omitted for brevity
}

Обратите внимание, что мы установили атрибут @OneToOne fetch в FetchType.ЛЕНИВЫЙ , по той же самой причине, которую мы объясняли ранее. Мы также используем @MapsId , потому что мы хотим, чтобы строка дочерней таблицы совместно использовала Первичный ключ со строкой родительской таблицы, что означает, что Первичный ключ также является внешним ключом для записи родительской таблицы.

Родительская Запись сущность выглядит следующим образом:

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

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToOne(
        mappedBy = "post", 
        cascade = CascadeType.ALL, 
        orphanRemoval = true, 
        fetch = FetchType.LAZY
    )
    private PostDetails details;

    //Getters and setters omitted for brevity

    public void setDetails(PostDetails details) {
        if (details == null) {
            if (this.details != null) {
                this.details.setPost(null);
            }
        }
        else {
            details.setPost(this);
        }
        this.details = details;
    }
}

подробности @OneToOne ассоциация помечена атрибутом mappedBy , который указывает, что сторона PostDetails отвечает за обработку этой двунаправленной ассоциации.

Метод setDetails используется для синхронизации обеих сторон этой двунаправленной связи и используется как для добавления, так и для удаления связанной дочерней сущности.

Поэтому, когда мы хотим связать Запись родительскую сущность с Деталями записи , мы используем метод setDetails :

Post post = new Post();
post.setTitle("High-Performance Java Persistence");

PostDetails details = new PostDetails();
details.setCreatedBy("Vlad Mihalcea");

post.setDetails(details);

entityManager.persist(post);

То же самое верно, когда мы хотим отделить Сообщение и Сведения о публикации сущности:

Post post = entityManager.find(Post.class, 1L);

post.setDetails(null);

Для получения более подробной информации о том, как лучше всего сопоставить @OneToOne ассоциацию, ознакомьтесь с этой статьей .

Давайте предположим, что сущность Post образует ассоциацию “многие ко многим” с Тегом , как показано на следующей диаграмме:

Тег отображается следующим образом:

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

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

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

    //Getters and setters omitted for brevity

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

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

Обратите внимание на использование аннотации @Naturally Hibernate, которая очень полезна для сопоставления бизнес-ключей.

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

Затем объект Post отображается следующим образом:

@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 Set tags = new LinkedHashSet<>();

    //Getters and setters omitted 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();
    }
}

Ассоциация теги @ManyToMany отвечает за обработку этой двунаправленной ассоциации, и это также причина, по которой сообщения | @ManyToMany ассоциация в Теге сущности помечена атрибутом mappedBy .

Методы добавить тег и удалить тег используются для синхронизации двунаправленной связи. Поскольку мы полагаемся на метод remove из интерфейса Set , как Тег , так и Сообщение должны правильно реализовывать равно и Хэш-код . В то время как Tag может использовать естественный идентификатор, сущность Post не имеет такого бизнес-ключа. По этой причине мы использовали идентификатор сущности для реализации этих двух методов, как описано в этой статье .

Чтобы связать объекты Post и Tag , мы можем использовать метод addTag , подобный этому:

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);

Чтобы отделить объекты Post и Tag , мы можем использовать метод удалить тег :

Post post1 = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "join fetch p.tags " +
    "where p.id = :id", Post.class)
.setParameter( "id", postId )
.getSingleResult();

Tag javaTag = entityManager.unwrap(Session.class)
.bySimpleNaturalId(Tag.class)
.getReference("Java");

post1.removeTag(javaTag);

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

Вот и все!

Всякий раз, когда вы используете двунаправленную ассоциацию JPA, она обязательна для синхронизации обоих концов отношения сущности.

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

Так что избавьте себя от лишних хлопот и поступайте правильно.