PreDeleteEventListener: veto deletion of entity with relation

Hello everyone,

My goal is to implement a custom version of soft-deletion.

I am aware that with hibernate-orm 7, timestamp-based soft-deletion is supported out-of-the-box. However, I retrieve the “sys_valid_to” timestamp dynamically, so I was unable to fulfill my usecase using that feature.

My current attempt is registering a PreDeleteEventListener, in which I return true to veto the deletion, and instead manually set my “sys_valid_to” field. It looks something like this:

private boolean onPreDelete(PreDeleteEvent event) {
    int validToIndex = -1;
    for (int i = 0; i < event.getPersister().getPropertyNames().length; i++) {
        if (event.getPersister().getPropertyNames()[i].equals("sysValidTo")) {
            validToIndex = i;
            break;
        }
    }

    Instant now = myContext.getTimestamp();

    Object[] deletedState = event.getDeletedState();
    Object[] oldDeletedState = new Object[deletedState.length];
    System.arraycopy(deletedState, 0, oldDeletedState, 0, deletedState.length);

    event.getPersister().setValue(entity, validToIndex, now);
    deletedState[validToIndex] = now;

    // Instead of actually deleting the entity, we instead issue an UPDATE that sets the entity's sys_valid_to to 'now'
    event.getPersister().getUpdateCoordinator().update(
        entity, event.getId(), null, deletedState, event.getPersister().getVersion(entity),
        oldDeletedState,
        new int[]{validToIndex},
        false, event.getSession());

    return true; // veto deletion: we performed our soft-delete above and wand no actual DELETE to be executed
}

This works fine for regular entities, but let’s consider an entity that has a relation like this:

@Table(name = "T_PARENT_ENTITY")
@Entity
public class MyParentEntity {

    @Id
    @Column(name = "PARENT_ID")
    private Long id;

    @Column(name = "SYS_VALID_TO")
    private Instant sysValidTo;

    @ManyToMany
    @JoinTable(
        name = "T_PARENT_TO_CHILD",
        joinColumns = @JoinColumn(name = "PARENT_LIST", nullable = false),
        inverseJoinColumns = @JoinColumn(name = "CHILD_ELEMENT", nullable = false)
    )
    private Set<MyChildEntity> children;
}

Now, the deletion of the MyParentEntity gets vetoed and prevented, but the deletion of any rows in the T_PARENT_TO_CHILD proceed. In fact, enabling SQL printing shows that those rows get deleted before the PreDeleteEventListener even runs; if I don’t veto the deletion, this is the chronology of statements:

[Hibernate] 
    delete 
    from
        T_PARENT_TO_CHILD
    where
        PARENT_LIST=?
...
[Hibernate] 
    delete 
    from
        T_PARENT_ENTITY
    where
        PARENT_ID=?

which makes sense of course, constraint-wise.

The PreCollectionRemoveEventListener event looked promising, but due to the fact that this one unfortunately does not allow any vetoing (the return type is void), I had no luck with it either.
Regarding the order, I could probably get away with looking at the event’s getAffectedOwnerOrNull to determine whether I need a veto there or not, even before my PreDeleteEventListener visited the owner.

Is there a way to “fully” veto the deletion of an entity?

What does that mean, can you please elaborate? Do you have a formula for the deleted timestamp? If so, how does that look and work? Do you set that yourself somehow through delete triggers?

Do you know that there is a SoftDeleteType.TIMESTAMP which would do exactly what you’re describing here? Also see Hibernate ORM User Guide

There is no supported way to veto the update of a collection. AFAICT, you’d have to provide a custom CollectionPersister to achieve that and dive into the guts of Hibernate ORM.

Do you know that there is a SoftDeleteType.TIMESTAMP which would do exactly what you’re describing here?

I am aware, yes. That feature almost does what I need it to do, but just almost. What I meant by the “sys_valid_to” timestamp being retrieved dynamically is basically this:

-- This is technically what @SoftDelete(strategy = SoftDeleteType.TIMESTAMP) does:
UPDATE mytable SET deleted = CURRENT_TIMESTAMP
-- But this is what I need
UPDATE mytable SET deleted = :deleted_at

With “deleted_at” being a timestamp provided at runtime by a @TransactionScoped CDI bean. In the example code above, that bean was called myContext. It’s done this way so that we can create one timestamp at the start of a transaction and use that timestamp for all deletions within that transactions.
I thought about subclassing the Dialect and override currentTimestamp, but mocking away the database’s CURRENT_TIMESTAMP feels like it will just introduce other problems.

It seems like you don’t understand how CURRENT_TIMESTAMP works within a transaction, because that’s exactly what the semantics are.

It seems like you don’t understand how CURRENT_TIMESTAMP works within a transaction, because that’s exactly what the semantics are.

I wish they were, but no, at least not in oracle:

MYDB_OWNER> declare
                t1 timestamp;
                t2 timestamp;
            begin
                select current_timestamp into t1 from dual;
                dbms_session.sleep(1);
                select current_timestamp into t2 from dual;
                if t1 = t2 then
                    dbms_output.put_line('Timestamps are equal: ' || t1);
                else
                    dbms_output.put_line('Timestamps are different: ' || t1 || ' and ' || t2);
                end if;
            end;
[2025-07-17 17:22:59] completed in 1 s 8 ms
Timestamps are different: 17.07.25 15:22:58,158703 and 17.07.25 15:22:59,158860

It looks like you are right. I was working with PostgreSQL for too long it seems: PostgreSQL: Documentation: 11: 9.9. Date/Time Functions and Operators

It seems though, that this SQL fragment could produce a transaction time on Oracle if you have the necessary permissions:

SELECT tx.START_DATE
FROM v$transaction tx 
JOIN v$session s ON tx.addr=s.taddr 
WHERE s.sid = sys_context('userenv','sid')

If you put that into your dialect, you could get around this AFAIU e.g.

public class MyOracleDialect extends OracleDialect {
	@Override
	public String currentTimestamp() {
		return "(SELECT tx.START_DATE FROM v$transaction tx JOIN v$session s ON tx.addr=s.taddr WHERE s.sid = sys_context('userenv','sid'))";
	}
}

That looks promising, I’ll give it a shot soon, thanks!

No luck so far. Your idea falls flat for me because I checked and we won’t be able to give our database access user those permissions.
Of course, I could still try to overwrite the dialect and supply an application-provided currentTimestamp() (e.g. by having it return a constant TO_TIMESTAMP(...some constant...)). Given that other databases like Postgres and apparently also H2 do guaranteee a stable CURRENT_TIMESTAMP per transaction, that alleviates my worries of it being a bad idea.

However, I still went ahead and tried to make @SoftDelete working, but was stopped by a whole different issue:

org.hibernate.MappingException: Column ‘SYS_VALID_TO’ is duplicated in mapping for entity ‘org.acme.MyEntity’ (use ‘@Column(insertable=false, updatable=false)’ when mapping multiple properties to the same column)

But we do need to have that column mapped in an insertable, updatable way.

If you’re interested in the why: We’re emulating system versioned tables application-side, since oracle’s AS OF TIMESTAMP operates on the undo tablespace but we need to be able to go back arbitrarily far into the past. For this we have a PreUpdateEventListener that captures the pre-update state and inserts it as an old version, setting the SYS_VALID_TO to “now”.

My gut tells me to not go down this particular rabbit hole any further and instead think about alternative solutions, but any suggestions are welcome. So far, thanks for sticking with me.

Are you saying that you want to manually update the value of the column? Because that certainly isn’t supported out of the box. Updating the column through native queries would be the way to go for now, though we’d be curious to hear about your use case. You’re not the first one to ask for this and we might add features to cover more use cases once we know the bigger picture.

I’d be curious to hear more about this. In fact, I brought up the idea to rethink Hibernate Envers on top of system/application versioned tables, but so far, we didn’t dig into that further yet.

It sounds to me like you try to re-implement what Hibernate Envers does though. Note that it does take care of inserting data into separate tables and can also be configured to store such timestamps: Hibernate ORM User Guide