Автор оригинала: 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 -> { Listtags = 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 Listcomments = 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 может упростить эту задачу для вас.