1

I need to add soft delete feature for all entities that my application manages. In order to accomplish that I created a base entity class(MappedSuperclass) with deleted field as below:

@MappedSuperclass
public abstract class BaseVersionableEntity {

    @Id
    @Basic(optional = false)
    @NotNull
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID", nullable = false)
    protected Long id;

    @Version
    @Column(name = "VERSION", nullable = false)
    protected Long version;

    @Basic
    @Column(name = "DELETED")
    protected boolean deleted;

    public boolean isDeleted() {
        return deleted;
    }

    public void setDeleted(boolean deleted) {
        this.deleted = deleted;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

    // equals/hashCode

}

By using hibernate's interceptor mechanism I configured a soft delete interceptor in order to catch all queries that hibernate executes:

public class SoftSelectInterceptor extends EmptyInterceptor{

    private static final long serialVersionUID = 1L;

    @Override
    public String onPrepareStatement(String sql) {

        if(sql.startsWith("select")){
            sql = sql.replace("where ","where deleted=0 and ");
        }

        return super.onPrepareStatement(sql);
    }
}

I added below interceptor config to persistence.xml file

<property name="hibernate.ejb.interceptor" value="com.my.organization.SoftSelectInterceptor"/> 

Note that there is a SoftDeleteInterceptor as well. Works with the same way.

When I execute this code hibernate catches select statement calls in onPeraparedStatement method but it adds a table alias in front of the table fields. That is if table name is reportTemplate, hibernate give this table reportTemplate0 alias. So my replace statement needs to be like below:

sql = sql.replace("where ","where reportTemplate0.deleted=0 and ");

I can parse the sql string and try to infer correct table aliases. However in complex sql statements (when same entity used multiple times) it would be hard to infer.

I wonder if there is a better way to do this.

Tevfik Kiziloren
  • 147
  • 3
  • 10

2 Answers2

1

I see three options:

  1. Use Hibernate Filters. Major disadvantage: You have to attach the filter for every entity.

    @Entity
    // The FilterDef can be declared at package level as well:
    @FilterDef(name="alive", defaultCondition="deleted = 0")
    // Turn on the filter for our entity:
    @Filter(name="alive")
    public class MyEntity { ... }
    
  2. Use a SQL parser (there are enough out there), modify the parsed result and recreate the SQL. Major disadvantage: There are enough not so trivial statements (including subselects) to modify. You have to be sure to add the restriction only to affected tables.

  3. Use a service architecture with a base service, where you always use the same methods for loading/finding entities, which builds the correct query. Major disadvantage: You can't use named queries.
Tobias Liefke
  • 8,637
  • 2
  • 41
  • 58
  • option 1 and 3 are not applicable for our case since I am supposed to provide a generic solution with minimal effect. I am considering option 2. Is there any global configuration parameter to specify the structure of table alieses. Such as use t prefix (like t1, t2) or use alwayse entity class name prefix etc? – Tevfik Kiziloren Jun 30 '15 at 14:37
  • There are no specific configuration parameters for aliasing. Usually it is _Entity Name_ + _a counter_ + `_` (see `org.hibernate.hql.ast.util.AliasGenerator.createName(String)`), but I can't guarantee that for all generated SQLs. The documentation says _Subclasses of Loader do *not* have to use aliases of this form_ – Tobias Liefke Jun 30 '15 at 15:16
  • I accepted your answer since option 1 is always applicable if we disregard the effort. one last question: Hibernate supports Filter annotation in MappedSupperClasses(since version 3.5.0). what if I used only one filter in mapped superclass and and enforce all of the entities in the project inherit this superclass. Are there any disadvantages of this approach? – Tevfik Kiziloren Jun 30 '15 at 15:50
  • You should check if the filter is applied to collections of the entities as well. As far as I know it is possible to set a filter for collections. And I don't know if you _have to_ set it to enforce the filter. – Tobias Liefke Jun 30 '15 at 16:07
  • Filters need to be declared on collections as well. Also, keep in mind that filters take no effect to `ToOne` associations (searching by id). This could be a problem if a non-deletable entity is associated in a `OneToOne` association with a deletable entity and the foreign key column is not nullable. Then change it to be nullable and make sure to break the association when soft deleting the deletable instance (by setting null on the owning side of the association). – Dragan Bozanovic Jul 01 '15 at 01:21
0

You could map the deletable entities to database views which select from the original tables with the filter condition (deleted = 0).

EDIT

Unfortunately, it's impossible to do it the way you are trying to.

1) There is no way to exactly predict what the aliases will look like, especially with complex queries with multiple joins and subqueries. Even if you analyze the source code, it is not part of the public API and as such is subject to change.

However, if you decide to take that endeavor (analyze the Hibernate source for query parsing and SQL generation), keep in mind that you will probably spend your entire summer doing only that since it is a complex and huge code base and a few very important feature requests (like left joins for non-associated entities) are waiting for years for it to be refactored.

2) The queries (and subqueries) without the where clause will not exclude deleted rows (since your replacement will find no matches):

select c from MyEntity;

3) Less likely for where combination of letters but still possible, where might be part of an identifier or a text constant and your replacement would make the query invalid. Suppose someone adds two fields to a Trip entity: fromWhere and toWhere. Maybe not a good naming example, but you get the point.

You could solve it by replacing only occurrences of the whole where word, but then imagine a query like:

select ... from ... where aColumn = 'A constant with the where word in it'
Community
  • 1
  • 1
Dragan Bozanovic
  • 23,102
  • 5
  • 43
  • 110
  • I thought about that answer as well - but ignored it as it depends on the database if the entity may be changed through a view. And I can't use schema generation for those views and (more important) for the tables, at least I would need additional conversion. – Tobias Liefke Jun 30 '15 at 15:03
  • As far as I understand, there is no solution without pain. Thanks for the edit @Dragan – Tevfik Kiziloren Jul 01 '15 at 08:48