How to manage the relationships of @Any and @ManyToAny entities when deleting?

:tada: For anyone else who has run into this issue, I’ve updated my Github project with a solution:

I was able to piece together a solution for Hibernate to execute some code in the event an entity is deleted. This was thanks to the Medium article mentioned above, by Josh Harkema, and a post on Vlad Mihalcea’s website. Start by creating a PreDeleteEventListener whose onPreDelete() method will handle the PreDeleteEvent Hibernate issues before deleting an entity.

public class ApplicationPreDeleteEventListener implements PreDeleteEventListener {

    private final PropertyEventHandler propertyEventHandler = new PropertyEventHandler();


    @Override
    public boolean onPreDelete(PreDeleteEvent event) {
        Object entity = event.getEntity();
        boolean veto = entity == null;

        if (! veto && entity instanceof Property<?> property) {
            veto = propertyEventHandler.preDelete(property, event.getSession());
        }

        return veto;
    }
}

The onPreDelete() method returns a boolean that tells Hibernate whether to veto the original delete that triggered the event.
In this implementation, the PropertyEventHandler uses the provided EventSource to remove relationships from the given Property prior to its deletion:

public class PropertyEventHandler {

    public boolean preDelete(Property<?> property, EventSource eventSource) {
        return removeFromPropertyHolders(property, eventSource) ||
                removeFromPropertyRepositories(property, eventSource);
    }

    protected boolean removeFromPropertyHolders(Property<?> property, EventSource eventSource) {
        eventSource.getSession()
                   .createNativeMutationQuery("""
                           UPDATE property_holder
                              SET property_type = NULL
                                , property_id = NULL
                            WHERE property_id = :property_id
                           """)
                   .setParameter("property_id", property.getId())
                   .setHibernateFlushMode(FlushMode.MANUAL)
                   .executeUpdate();
        return false;
    }

    protected boolean removeFromPropertyRepositories(Property<?> property, EventSource eventSource) {
        eventSource.getSession()
                   .createNativeMutationQuery("""
                           DELETE FROM repository_properties
                            WHERE property_id = :property_id
                              AND property_type = :property_type
                           """)
                   .setParameter("property_id", property.getId())
                   .setParameter("property_type", property.getDiscriminator())
                   .setHibernateFlushMode(FlushMode.MANUAL)
                   .executeUpdate();
        return false;
    }
}

In order to enable this mechanism in the project, the ApplicationPreDeleteEventListener needs to be “integrated” into Hibernate.

:information_source: There may be other ways of “integrating” event listeners into Hibernate, but this one worked for me.

Create an implementation of Integrator:

public class ApplicationIntegrator implements Integrator {

    @Override
    public void integrate(@UnknownKeyFor @NonNull @Initialized Metadata metadata,
                          @UnknownKeyFor @NonNull @Initialized BootstrapContext bootstrapContext,
                          @UnknownKeyFor @NonNull @Initialized SessionFactoryImplementor sessionFactory) {

        final EventListenerRegistry eventListenerRegistry =
                sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);

        eventListenerRegistry.appendListeners(EventType.PRE_DELETE, ApplicationPreDeleteEventListener.class);
    }

    @Override
    public void disintegrate(@UnknownKeyFor @NonNull @Initialized SessionFactoryImplementor sessionFactoryImplementor,
                             @UnknownKeyFor @NonNull @Initialized SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
        // We HAVE to override this...
    }
}

:information_source: The @UnknownKeyFor, @NonNull and @Initialized annotations on the integrate() method are from the org.checkerframework:checker-qual package, which is necessary to create an Integrator.

Next, create an implementation of IntegratorProvider:

public class ApplicationIntegratorProvider implements IntegratorProvider {

    @Override
    public List<Integrator> getIntegrators() {
        return List.of(new ApplicationIntegrator());
    }
}

Finally, tell Hibernate to use the ApplicationIntegratorProvider. If using Hibernate alone, append this to the project’s persistence.xml:

<persistence ...>
    <persistence-unit ...>
        ...
        <properties>
            ...
            <property name="hibernate.integrator_provider" value="com.example.hibernatepolymorph.config.ApplicationIntegratorProvider"/>
        </properties>
    </persistence-unit>
    ...
</persistence>

If using Spring Boot, append this to the application.properties:

spring.jpa.properties.hiberrnate.integrator_provider=package.name.to.ApplicationIntegratorProvider

Now, all of the project’s tests perform as expected. :tada:

1 Like