Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logical deletes are not cascaded to child entities #2

Open
vahidpaz opened this issue Jan 11, 2014 · 4 comments
Open

Logical deletes are not cascaded to child entities #2

vahidpaz opened this issue Jan 11, 2014 · 4 comments

Comments

@vahidpaz
Copy link

When a parent entity is logically deleted, its children (as defined by GORM's "belongsTo") are not visited at all.

As an alternative some might want to use the Hibenerate Filter plugin, along with the GORM beforeDelete() method: http://stackoverflow.com/questions/12467407/soft-delete-an-entity-in-grails-with-hibernate-filters-plugin

However I have had others problems with the Hibernate Filter plugin. I'm curious to know if Logical Delete is planning to support cascading. Thanks.

@vahidpaz
Copy link
Author

Just as an update: I have forked this project into a private location and have updated the code to successfully cascade deletes.

My current solution is not a great one, but it works. I disabled the execution of LogicalDeleteDomainClassEnhancer.enhance(). Then I created an AST transform to inject a beforeDelete() into all domain classes annotated with @LogicalDelete. See snippet below. My understanding of Grails transactions and sessions is also limited, so you may be able to make this more performant too.

As you'll see in the code's TODO, this solution is limited because the user cannot define their own beforeDelete trigger. Technically they can but the behavior can get complicated if I allowed it since they can have beforeDelete() and beforeDelete(Map). I suggest using one of two other techniques discussed on the Grails docs page, such as PreDeleteEventListener: http://grails.org/doc/latest/guide/GORM.html#eventsAutoTimestamping

class GLogicalDeleteASTTransformation {
    static addGrailsTriggerToLogicallyDelete(ClassNode node, SourceUnit source) {
        ensureNoExistingBeforeDeleteTriggers(node, source)

        def methodBodyAst = new AstBuilder().buildFromCode {
            executeUpdate("UPDATE ${this.getClass().name} SET deleted = 1 WHERE id = :id", [id: id])

            // Also update the in-memory entity since we are using SQL for the update, otherwise it becomes dirty.
            deleted = 1

            return false
        }

        def method = new MethodNode("beforeDelete", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, [] as Parameter[], [] as ClassNode[], (Statement) methodBodyAst[0])
        node.addMethod(method)
    }

    // TODO: Consider using Hibernate's PreDeleteEventListener or Grails's AbstractPersistenceEventListener to allow the client to use their own beforeDelete triggers. See: http://grails.org/doc/latest/guide/GORM.html
    private static ensureNoExistingBeforeDeleteTriggers(ClassNode node, SourceUnit source) {
        if (node.methods.any { it.name == "beforeDelete" }) {
            source.addException(new RuntimeException("Logical Delete plugin: Your class '$node.name' cannot have its own beforeDelete() method defined"))
        }
    }
}

@lmadeira
Copy link

I did it using Hibernate Events and commenting LogicalDeleteDomainClassEnhancer.enhance() method. See below.

at resources.groovy:

beans = {
    auditListener(MyPreDeleteEventListener)

    hibernateEventListeners(HibernateEventListeners) {
        listenerMap = ['pre-delete': auditListener]
    }
}
class MyPreDeleteEventListener implements PreDeleteEventListener{
    protected final Logger log = LoggerFactory.getLogger(getClass())

    @Override
    public boolean onPreDelete(PreDeleteEvent evt) {
        if(mustBeEnhanced(evt.entity.class)){
            try{
                evt.entity.withNewSession { session ->
                    session.get(evt.entity.class, evt.entity.id).executeUpdate("UPDATE ${evt.entity.class.name} SET deleted = 1 WHERE id = :id", [id: evt.entity.id])
                }
            } catch(Exception e) {
                log.error(e.getMessage(), e)
            }

            return true
        }
        return false;
    }

    private static boolean mustBeEnhanced(clazz){
        LogicalDeleteDomainClass.isAssignableFrom(clazz)
    }
}

@vahidpaz
Copy link
Author

Thanks for the code @lmadeira.

I gave it a try but there was a conflict with the Envers plugin that I'm using. My tests seem to indicate that only one "hibernateEventListeners" bean can be created and only that been is honored by Grails to easily create HibernateEventListeners. I tried changing Logical Delete to loadBefore and loadAfter 'envers' and I noticed that either Envers worked, or Logical Delete, but not both.

Not knowing much about how to register my own Hibernate event listeners without using the "hibernateEventListeners" bean I looked through the web for help and found some hope: http://www.javacodegeeks.com/2012/10/stuff-i-learned-from-grails-consulting.html. However the solution described there didn't work either because for some reason sessionFactory.getEventListeners() was not a valid method/property of my session factory (maybe a conflict with one of the other plugins I use, not sure).

So then I stumbled across a plugin which used a different facility to register its listeners: https://github.com/robertoschwald/grails-audit-logging-plugin/blob/master/grails-audit-logging-plugin/src/groovy/org/codehaus/groovy/grails/plugins/orm/auditable/AuditLogListener.groovy

That lead me to finally read the Grails docs :)
=> http://grails.org/doc/latest/guide/GORM.html#advancedGORMFeatures (see "Custom Event Listeners")

Now everything is working well: compatible with Envers, and my domain classes can have their own beforeDelete() methods defined.

My code is below. I'm using a String type for the "deleted" property because I need to support UUIDs as well as Numbers.

In the plugin's descriptor:

    def doWithApplicationContext = { ctx ->
        // See docs on persistence event system: http://grails.org/doc/latest/guide/GORM.html#advancedGORMFeatures
        application.mainContext.eventTriggeringInterceptor.datastores.each { k, datastore ->
            ctx.addApplicationListener new LogicalDeletePersistenceEventListener(datastore)
        }
    }

Then the new listener class itself:

package com.nanlabs.grails.plugin.logicaldelete

import groovy.util.logging.Slf4j
import org.grails.datastore.mapping.core.Datastore
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener
import org.grails.datastore.mapping.engine.event.EventType
import org.grails.datastore.mapping.engine.event.PreDeleteEvent
import org.springframework.context.ApplicationEvent

import static com.nanlabs.grails.plugin.logicaldelete.LogicalDeleteDomainClassEnhancer.mustBeEnhanced

@Slf4j
class LogicalDeletePersistenceEventListener extends AbstractPersistenceEventListener {
    protected LogicalDeletePersistenceEventListener(Datastore datastore) {
        super(datastore)
    }

    @Override
    protected void onPersistenceEvent(AbstractPersistenceEvent event) {
        if (!mustBeEnhanced(event.entityObject.getClass())) return

        switch (event.eventType) {
            case EventType.PreDelete:
                onPreDelete(event as PreDeleteEvent)
                break
        }
    }

    @Override
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        eventType.isAssignableFrom(PreDeleteEvent)
    }

    protected onPreDelete(PreDeleteEvent event) {
        def entityObj = event.entityObject

        log.debug "onPreDelete(): entityObject.id=$entityObj.id, entityObj=$entityObj, type=${entityObj.getClass()}"

        def idAsString = entityObj.id as String

        // Perform database write operations in a new session, else we run the risk of triggering our event listener again (stack overflow).
        entityObj.withNewSession { session ->
            def entityObjInNewSession = session.get(entityObj.class, entityObj.id)
            entityObjInNewSession.executeUpdate("UPDATE ${entityObjInNewSession.getClass().name} SET deleted = :deleted WHERE id = :id", [deleted: idAsString, id: entityObjInNewSession.id])
        }

        // Update in-memory object, else it gets dirty/stale. This is assuming the caller flushed the Hibernate session before this event listener was invoked.
        entityObj.deleted = idAsString

        // Veto the physical delete event.
        event.cancel()
    }
}

Let me know if you can spot any issues with it. Thanks again to all contributors of this plugin.

@neoecos
Copy link

neoecos commented Mar 11, 2015

@vahidpaz Do you plan to pull request this plugin or provide a new one ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants