DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Implement Hibernate Second-Level Cache With NCache
  • Architectural Miscalculation and Hibernate Problem "Type UUID but Expression Is of Type Bytea"
  • How to Store Text in PostgreSQL: Tips, Tricks, and Traps
  • Simplify Java Persistence Using Quarkus and Hibernate Reactive

Trending

  • Integrating AI-Driven Decision-Making in Agile Frameworks: A Deep Dive into Real-World Applications and Challenges
  • The Death of "Text-Only" ChatOps: Why Google's A2UI Matters for DevOps and SRE
  • How to Prevent Data Loss in C#
  • Why Your QA Engineer Should Be the Most Stubborn Person on the Team
  1. DZone
  2. Data Engineering
  3. Databases
  4. Soft Deletion in Hibernate: Things You May Miss

Soft Deletion in Hibernate: Things You May Miss

The article explains some details that stay hidden by most of the articles about soft deletion implementation using Hibernate.

By 
Aleksey Stukalov user avatar
Aleksey Stukalov
·
Andrey Oganesyan user avatar
Andrey Oganesyan
·
Updated Aug. 31, 21 · Tutorial
Likes (11)
Comment
Save
Tweet
Share
10.7K Views

Join the DZone community and get the full member experience.

Join For Free

Soft Deletion is a commonly used pattern in enterprise applications. Hibernate is a widely used persistence framework in the Java ecosystem. Therefore, it is not surprising that Hibernate provides a way to implement soft deletion. However, this approach has some awkward aspects, which are not highlighted by the docs and articles on that matter. In this article, we will dive into some details that you may miss.

Hard Delete vs. Soft Delete

@SQLDelete + @Where

If you Google 'soft deletion hibernate,' you’ll most likely find tutorials by Eugen Paraschiv, Vlad Mihalcea, or Thorben Janssen. They suggest using Hibernate @SQLDelete and @Where annotations which let you automatically set the deleted flag and filter by it:

@Entity
@Table(name = "article")
@SQLDelete(sql = "update article set deleted=true where id=?")
@Where(clause = "deleted = false")
public class Article {
   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE)
   @Column(name = "id", nullable = false)
   private Long id;

   @Column(name = "deleted", nullable = false)
   private Boolean deleted = false;

   // other properties, getters and setters omitted
}

@SQLDelete takes a native SQL query that is executed whenever a Hibernate-managed entity gets deleted. @Where takes a condition and appends it to 'select' queries automatically, allowing you to filter entities based on the deleted attribute automatically.

This looks like a silver bullet, however, the devil is in the details. Plain queries will work absolutely fine, but what's going to happen with associations?

Problems With Associations

Let's think for a second, what behaviour would you expect when you fetch an entity that has an association field to a collection, where some entities were soft-deleted, or to a single soft-deleted entity? In fact, there is no wide range of options. Rather, you want deleted records to be excluded or included in the resulting dataset. And your choice may vary depending on the use case. E.g., we have a collection of items in the e-store cart. Deleting an item we would expect this to disappear from the order, right? However, deleted products should stay in the historical invoices. Isn't it a contradiction?

Let's examine how the @Where + @SQLDelete solution works for different types of associations, fetch the type and API used for executing the query. See the ER diagram below, we are going to use it for our further experiments:

ER Diagram

The Article has a ManyToMany association to Author, OneToMany association to Comment, and a OneToOne association to ArticleDetails. The question is, what should happen if one of them is deleted but still referenced from a live entity?

OneToMany and ManyToMany

Hibernate filters out deleted entities from all ToMany associations. If you run the following code before and after marking one author entity as deleted, the number of names printed will change:

articleOptional.ifPresent(article -> {
   article.getAuthors()
           .forEach(author -> logger.info(author.getName()));
});

The good news, such behavior consistently happens regardless of the fetching type (lazy or eager) and way of calling it (via entityManager, Criteria API, Spring Data JPA, etc.).

Lazy ManyToOne and OneToOne

Let's imagine that we soft-delete an article from our example. However, we don't want to delete comments under this article, so that when recovering the article it appears back with its comments.

@Entity
@Table(name = "comment")
public class Comment {
   ...
   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "article_id")
   private Article article;
   ...
}


Now, let's try to fetch a comment associated with the soft-deleted article:

Optional<Comment> comment = commentRepository.findById(id);
comment.ifPresent(com -> logger.info(com.getArticle().getText()));

If you add a breakpoint after the first line, you’ll see that the article is initialized with a proxy.

Well, this is understandable since Hibernate does not know in advance whether the entity is deleted or not. Once you call com.getArticle().getText(), the EntityNotFoundException is thrown! Is that what you would expect?

Eager ManyToOne and OneToOne

Let’s repeat the same experiment but change the fetch type to the Eager mode. Now comments get fetched with their article and no proxy is required. Hibernate knows for sure that the article is deleted. Let's run the same test:

Optional<Comment> comment = commentRepository.findById(id);
comment.ifPresent(com -> logger.info(com.getArticle().getText()));

The associated soft-deleted article is silently loaded with no exception with the deleted attribute set to true:

Deleted Attribute Set to True

Such inconsistent behavior can be easily explained. The eager fetch type makes Hibernate join the article table right away. So, Hibernate has a choice to throw the EntityNotFoundException right away or load it normally. Since the article is loaded, the @Where mechanism is out of power, and Hibernate simply maps it to the target class.

Let's fetch a collection of comments:

Iterable<Comment> comments = commentRepository.findAll();

Now we get EntityNotFoundException again! This starts to happen again because findAll leads to separate queries for the associated Article entities, as noted here.

Any time a soft-deleted entity gets fetched by a separate query it causes the above exception. This happens because generating a separate query Hibernate applies the @Where clause, which makes it impossible to find a soft-deleted entity. Obviously, such queries always return an empty result, which, in its turn, causes EntityNotFoundException.

What's even more amusing is that you need to experiment to see how exactly Hibernate will fetch data using different APIs. So, using QueryDSL you are going to hit the same exception fetching a collection or a single entity. At the same time, criteria API returns the deleted entity for eager OneToOne but throws EntityNotFoundException for eager ManyToOne. Total mess, isn't it?

Way to Avoid EntityNotFoundException

There is a cure against the annoying EntityNotFoundException. Hibernate introduces the @NotFound annotation, which can change the behaviour from raising the exception to silently setting null into the association field.

This may look like a panacea, however, it brings a significant downside: all ToOne associations become eagerly fetched, regardless of the declared fetch type. This fact may massively impact the performance of your application.

Info About Associations and Fetch

Problems with Constraints and Indexes

Both deleted and live entities will share unique constraints and indexes. Hence, creating regular indexes will not work any longer. Let's take an example where an author has a unique login constraint. After soft-deletion, our record stays in the table, so no live author can reuse the same login of the deleted ones.

You are lucky if you use PostgreSQL and can use partial indexes:

CREATE UNIQUE INDEX author_login_idx ON author (login) WHERE deleted = false;

But if you build your application on top of MySQL, this task turns out to be unsolvable.

Conclusion

As you may see, Soft Deletion is an easy pattern to understand, but not that easy to implement. Seems there is no ideal implementation for soft deletion. At least Hibernate doesn't provide one.

In simple cases, you definitely can use @SQLDelete + @Where. However, in the case when soft-deleted entities appear in OneToOne and ManyToOne associations, you barely can count on the consistent behaviour. Simply changing the fetch type, or introducing @EntityGraph, or porting your query from Criteria API to QueryDSL or whatever else is likely to change the result: from getting the unexpected exception to the unexpected successful load of the deleted entity or even getting the unexpected null.

Hibernate Database

Opinions expressed by DZone contributors are their own.

Related

  • Implement Hibernate Second-Level Cache With NCache
  • Architectural Miscalculation and Hibernate Problem "Type UUID but Expression Is of Type Bytea"
  • How to Store Text in PostgreSQL: Tips, Tricks, and Traps
  • Simplify Java Persistence Using Quarkus and Hibernate Reactive

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook