Monday, October 4, 2021

Grails 4 - how to attach domain entity manually to session and marked as readonly mode in hibernate session

Basically when an entity loaded from database via hibernate transaction manager then automatically attached to hibernate session.

If we use Domain.read() then domain entity attached with hibernate session but marked as readonly mode.

In such case domain don't get updated on any change, because they marked as readonly mode.

But what will happen if you want to attach entity in hibernate session manually and mark them as readonly mode.
To understand check below example:

Account account = new Account()
account.id = 10 
// we already know that there is an entity with id 10
// and we don't want to fetch data from database

Order order = new Order()
order.account = account
order.save()
Above example will execute but when the statement [order.account = account] executed, a fetch operation will take place like:

select account.id,account..... from account where account.id=10 because account entity is not attached with current hibernate session.

To overcome this problem we will manually attach account entity to hibernate session and mark them as readonly state using:
import org.hibernate.internal.SessionImpl

Account.withSession { SessionImpl session ->
	account.attach()
	session.setReadOnly(account, true)
	// readonly entity will not persist in database
}
Now when saving order, account entity already attached with hibernate session and no additional query will be executed.

This mechanism will help you when you know that an entity already exising and you don't want to fetch from db.
Here is another fantastic solution I hope you will like this idea.

As of latest hibernate version, all domain entities must be attached to session (previous hibernate version don't recommend this)

So check below case:

Account account = new Account()
account.id = 10 
// we already know that there is an entity with id 10
// and we don't want to fetch data from database

Order order = new Order()
order.account = account


When assign account to order, system expect account must be attached with session as account and order both are domain entities.

And because of account is not attached with current session, an select query will took place here to fetch data from database.
To avoid this we can do some simple trick so that without reading one entity from database we can continue our operation.

At first we need to add a Map field to each domain class to hold some value like:

class Account implements MyDomainInterface {
    Object TEMP = [:]
    Object ATTACHED_OBJECT_ENTITIES = [:]
    
    Long id

    void setAsCarbonCopy() {
        this.TEMP.existing = true
    }

    Boolean isCarbonCopy(Object o = null) {
        return this.TEMP.containsKey("existing")
    }
}


You need to do same for order domain class.

Then we need to extend metaclass like:

import grails.core.GrailsApplication
import org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack

GrailsApplication grailsApplication

grailsApplication.domainClasses.each { domainClass ->
    domainClass.metaClass.setProperty = { String propertyName, propertyValue ->
        MetaProperty metaProperty = domainClass.metaClass.getMetaProperty(propertyName);
        if (metaProperty) {
            if (propertyValue instanceof MyDomainInterface && propertyValue.isCarbonCopy()) {
                //we will set an proxy instance rather then real entity
                //proxy entity by default attached with session
                //but no addition query run for data collection until call explicitly
                Object no = propertyValue.class.proxy(propertyValue.id)
                delegate.ATTACHED_OBJECT_ENTITIES[propertyName] = propertyValue
                metaProperty.setProperty(delegate, no)
            }
            else {
                metaProperty.setProperty(delegate, propertyValue)
            }
        }
        else {
            throw new MissingPropertyExceptionNoStack(propertyName, domainClass.metaClass.theClass)
        }
    }
    domainClass.metaClass.getProperty = { String propertyName ->
        if (propertyName != "ATTACHED_OBJECT_ENTITIES" && delegate.ATTACHED_OBJECT_ENTITIES.containsKey(propertyName)) {
            //we will return original domain entity 
            //rather than proxy entity
            return delegate.ATTACHED_OBJECT_ENTITIES[propertyName]
        }
        MetaProperty metaProperty = domainClass.metaClass.getMetaProperty(propertyName)
        return metaProperty.getProperty(delegate)
    }
}
Then do the logic as follows:

Account account = new Account()
account.id = 10 
account.setAsCarbonCopy()

Order order = new Order()
order.account = account
// now no additional query will run


You can get order.account which will return above pojo entity as we modified our meta method.

So any change on account and/or order.account will not automatically persist in our database as account is nothing but a pojo entity.

And is not attach with session, we attached Account.proxy() with hibernate session, not pojo entity.

No comments:

Post a Comment