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

Лучший способ мягкого удаления с помощью Hibernate

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

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

Так кто же использует эту технику?

Например, StackOverflow делает это для всех сообщений (например, Вопросов и ответов). Стековый поток Сообщения таблица имеет Дату закрытия столбец , который действует как механизм мягкого удаления, поскольку он скрывает ответ для всех пользователей, у которых репутация менее 10 тыс. .

Если вы используете Oracle, вы можете воспользоваться его возможностями Flashback , поэтому вам не нужно изменять код приложения, чтобы предложить такую функциональность. Другой вариант-использовать функцию Временная таблица SQL Server .

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

Учитывая, что у нас есть следующие таблицы в вашей базе данных:

Как вы можете видеть на диаграмме выше, сообщение , post_details , post_comment , и тег таблицы содержат удаленный столбец, который определяет видимость данной строки. Что интересно в этой табличной модели базы данных, так это то, что она охватывает все три типа отношений с базой данных:

  • один к одному
  • один ко многим
  • многие ко многим

Поэтому мы обсудим отображение всех этих сущностей, а также их взаимосвязи, так что следите за обновлениями!

Объект тега

Давайте начнем с сопоставления Тегов сущностей, так как в нем отсутствует какое-либо отношение к сущностям:

@Entity(name = "Tag")
@Table(name = "tag")
@SQLDelete(sql =
    "UPDATE tag " +
    "SET deleted = true " +
    "WHERE id = ?")
@Loader(namedQuery = "findTagById")
@NamedQuery(name = "findTagById", query =
    "SELECT t " +
    "FROM Tag t " +
    "WHERE " +
    "	t.id = ?1 AND " +
    "	t.deleted = false")
@Where(clause = "deleted = false")
public class Tag 
    extends BaseEntity {

    @Id
    private String id;

    //Getters and setters omitted for brevity
}

В столбце удалено определен класс BaseEntity , который выглядит следующим образом:

@MappedSuperclass
public abstract class BaseEntity {

    private boolean deleted;
}

Аннотация @SqlDelete позволяет переопределить значение по умолчанию УДАЛИТЕ инструкцию , выполняемую Hibernate, поэтому вместо нее мы заменяем инструкцию UPDATE . Таким образом, удаление сущности приведет к обновлению столбца удалено до true .

Аннотация @Loader позволяет нам настраивать запрос SELECT , используемый для загрузки объекта по его идентификатору. Следовательно, мы хотим отфильтровать каждую запись, для столбца удалено которой установлено значение true .

Предложение @Where используется для запросов сущностей, и мы хотим предоставить его, чтобы Hibernate мог добавить условие фильтрации удаленных столбцов, чтобы скрыть удаленные строки.

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

Итак, учитывая, что у нас есть четыре Тега сущности:

doInJPA( entityManager -> {
    Tag javaTag = new Tag();
    javaTag.setId("Java");
    entityManager.persist(javaTag);

    Tag jpaTag = new Tag();
    jpaTag.setId("JPA");
    entityManager.persist(jpaTag);

    Tag hibernateTag = new Tag();
    hibernateTag.setId("Hibernate");
    entityManager.persist(hibernateTag);

    Tag miscTag = new Tag();
    miscTag.setId("Misc");
    entityManager.persist(miscTag);
} );

При удалении Разное Тег :

doInJPA( entityManager -> {
    Tag miscTag = entityManager.getReference(Tag.class, "Misc");
    entityManager.remove(miscTag);
} );

Hibernate выполнит следующую инструкцию SQL:

UPDATE tag 
SET deleted = true 
WHERE id = 'Misc'

Блестяще!

Итак, теперь, если мы хотим загрузить сущность, вместо этого мы получаем null:

doInJPA( entityManager -> {
    assertNull(entityManager.find(Tag.class, "Misc"));
} );

Это связано с тем, что Hibernate выполнил следующую инструкцию SQL:

SELECT 
    t.id as id1_4_, 
    t.deleted as deleted2_4_ 
FROM 
    tag t 
WHERE 
    ( t.deleted = 0 ) AND 
    t.id = ? AND 
    t.deleted = 0

Хотя предложение удалено добавляется дважды , потому что мы объявили как предложение @Where , так и @Loader , большинство СУБД устранят дублирующие фильтры во время анализа запросов . Если мы предоставим только предложение @Where , не будет дублирующего предложения delete , но тогда строки deleted будут видны при прямой загрузке.

Кроме того, при выполнении запроса сущности для всех Налоговых сущностей мы видим, что теперь мы видим только три Тега :

doInJPA( entityManager -> {
    List tags = entityManager.createQuery(
        "select t from Tag t", Tag.class)
    .getResultList();

    assertEquals(3, tags.size());
} );

Это связано с тем, что Hibernate удается добавить фильтр предложений deleted при выполнении SQL-запроса:

SELECT 
    t.id as id1_4_, 
    t.deleted as deleted2_4_ 
FROM tag t 
WHERE ( t.deleted = 0 )

Сущность сведений о должности

Так же, как Тег , Подробности публикации следует тем же соображениям сопоставления:

@Entity(name = "PostDetails")
@Table(name = "post_details")
@SQLDelete(sql = 
    "UPDATE post_details " +
    "SET deleted = true " +
    "WHERE id = ?")
@Loader(namedQuery = "findPostDetailsById")
@NamedQuery(name = "findPostDetailsById", query = 
    "SELECT pd " +
    "FROM PostDetails pd " +
    "WHERE " +
    "  pd.id = ?1 AND " +
    "  pd.deleted = false")
@Where(clause = "deleted = false")
public class PostDetails 
    extends BaseEntity {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    public PostDetails() {
        createdOn = new Date();
    }

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    //Getters and setters omitted for brevity
}

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

Сущность комментария к сообщению

Та же логика применима к Комментарию к сообщению :

@Entity(name = "PostComment")
@Table(name = "post_comment")
@SQLDelete(sql =
    "UPDATE post_comment " +
    "SET deleted = true " +
    "WHERE id = ?")
@Loader(namedQuery = "findPostCommentById")
@NamedQuery(name = "findPostCommentById", query =
    "SELECT pc " +
    "from PostComment pc " +
    "WHERE " +
    "  pc.id = ?1 AND " +
    "  pc.deleted = false")
@Where(clause = "deleted = false")
public class PostComment 
    extends BaseEntity {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

Даже если он содержит @ManyToOne ассоциацию с Сообщением , нет необходимости фильтровать эту связь, поскольку дочерняя сущность не может существовать без своего родителя.

Почтовая организация

Сущность Post является корнем нашей совокупности сущностей, она имеет отношение к Деталям публикации , Комментарий к сообщению , и Тег

@Entity(name = "Post")
@Table(name = "post")
@SQLDelete(sql = 
    "UPDATE post " +
    "SET deleted = true " +
    "WHERE id = ?")
@Loader(namedQuery = "findPostById")
@NamedQuery(name = "findPostById", query = 
    "SELECT p " +
    "FROM Post p " +
    "WHERE " +
    "  p.id = ?1 AND " +
    "  p.deleted = false")
@Where(clause = "deleted = false")
public class Post 
    extends BaseEntity {

    @Id
    private Long id;

    private String title;

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

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

    @ManyToMany
    @JoinTable(
        name = "post_tag",
        joinColumns = @JoinColumn(name = "post_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private List tags = 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);
    }

    public void addDetails(PostDetails details) {
        this.details = details;
        details.setPost(this);
    }

    public void removeDetails() {
        this.details.setPost(null);
        this.details = null;
    }

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

Сопоставление Post сущности аналогично Тегу сущности, которое мы уже обсуждали, поэтому мы сосредоточимся на @OneToMany и @ManyToMany ассоциации.

Двунаправленная ассоциация @OneToMany

В то время как до Hibernate 5.2 необходимо было предоставить аннотацию @Where предложения для коллекций (например, “@OneToMany ” или “@ManyToMany”), в Hibernate 5.2 нам не нужны эти аннотации на уровне коллекции, поскольку “ПостКоммент” уже аннотирован соответствующим образом, и Hibernate знает, что ему нужно отфильтровать любой _deleted_ “ПостКоммент”.

Поэтому, предполагая, что у нас есть одна Запись сущность с двумя Комментарием к записи дочерними сущностями:

doInJPA( entityManager -> {
    Post post = new Post();
    post.setId(1L);
    post.setTitle("High-Performance Java Persistence");
    entityManager.persist(post);

    PostComment comment1 = new PostComment();
    comment1.setId(1L);
    comment1.setReview("Great!");
    post.addComment(comment1);

    PostComment comment2 = new PostComment();
    comment2.setId(2L);
    comment2.setReview("Excellent!");
    post.addComment(comment2);
} );

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

doInJPA( entityManager -> {
    Post post = entityManager.find(Post.class, 1L);
    post.removeComment(post.getComments().get(0));
} );

Каскадный механизм запустит удаление дочернего элемента, и Hibernate выполнит следующую инструкцию SQL:

UPDATE post_comment 
SET deleted = true 
WHERE id = 1

И теперь мы видим, что в коллекции есть только одна запись:

doInJPA( entityManager -> {
    Post post = entityManager.find(Post.class, 1L);
    assertEquals(1, post.getComments().size());
} );

При извлечении коллекции комментариев Hibernate выполняет следующий запрос:

SELECT 
    pc.id as id1_0_, 
    pc.deleted as deleted2_0_, 
    pc.title as title3_0_ 
FROM 
    post pc 
WHERE 
    ( pc.deleted = 0) AND 
    pc.id=1 AND 
    pc.deleted = 0

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

Двунаправленная ассоциация @ManyToMany

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

Итак, если у нас есть одна Запись сущность с тремя Тегами дочерними сущностями:

doInJPA( entityManager -> {
    Post post = new Post();
    post.setId(1L);
    post.setTitle("High-Performance Java Persistence");

    entityManager.persist(post);

    post.addTag(entityManager.getReference(
        Tag.class, "Java"
    ));
    post.addTag(entityManager.getReference(
        Tag.class, "Hibernate"
    ));
    post.addTag(entityManager.getReference(
        Tag.class, "Misc"
    ));
} );

doInJPA( entityManager -> {
    Post post = entityManager.find(Post.class, 1L);
    assertEquals(3, post.getTags().size());
} );

Если мы удалим один Тег :

doInJPA( entityManager -> {
    Tag miscTag = entityManager.getReference(Tag.class, "Misc");
    entityManager.remove(miscTag);
} );

Тогда мы больше не увидим его в коллекции теги :

doInJPA( entityManager -> {
    Post post = entityManager.find(Post.class, 1L);
    assertEquals(2, post.getTags().size());
} );

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

SELECT 
    pt.post_id as post_id1_3_0_, 
    pt.tag_id as tag_id2_3_0_, 
    t.id as id1_4_1_, 
    t.deleted as deleted2_4_1_ 
FROM post_tag pt 
INNER JOIN 
    tag t ON pt.tag_id = t.id 
WHERE 
    ( t.deleted = 0 ) AND 
    pt.post_id = 1

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