Search code examples
grailsclonegrails-orm

Proper Implementation of clone() For Domain Classes to duplicate a Grails domain instance


I have several domain classes for which the user interface includes a "duplicate" command. As part of the implementation of those commands, I have implemented clone() methods in the corresponding domain classes.

I have been trying to correct my bad habit of improperly implementing clone() (in general) based on use of "new" rather than "super.clone()," so as soon as I thought about doing the same for my Grails domain classes, I wondered how using super.clone() to obtain a clone might interact with GORM / Hibernate persistence. In particular, I was wondering about the proper way to handle the implicit "id" property. If I simply super.clone(), do nothing further and later try to save() the cloned instance, will it work properly (creating a new persistence entry?) or will some kind of error or silent failure result?

What is the proper or preferred way to duplicate a Grails domain instance?


Solution

  • Add the following method to metaClass of the interface GormInstanceApi which all domain implements it. :

    def cloneForDomains={def cloned=delegate.class.newInstance();
                     cloned.properties=delegate.properties;
                    return cloned;}
    

    then :

    org.grails.datastore.gorm.GormInstanceApi.clone=cloneForDomains ;
    

    Congrats! now you can use clone method such as save , delete .....

    USE CASE :

    Person p=Person.get(1);
    Person cloned=p.clone(); 
     cloned.id=null; 
    cloned.save();
    

    UPDATE : you can loop all domain classes also :

    grailsApplication.getDomainClasses().each{cls->
                 cls.metaClass.clone=cloneForDomains
             }
    

    UPDATE : for deep clone :

      grailsApplication.getDomainClasses().each{cls->
                     cls.metaClass.clone={
                                      return deepClone(delegate);   
                                }
       }
    

    known that deepClone is a method as following:

    Object deepClone(domainInstanceToClone) {
    
             //TODO: PRECISA ENTENDER ISSO! MB-249 no youtrack
             //Algumas classes chegam aqui com nome da classe + _$$_javassist_XX
             if (domainInstanceToClone.getClass().name.contains("_javassist"))
                 return null
    
             //Our target instance for the instance we want to clone
             // recursion
             def newDomainInstance = domainInstanceToClone.getClass().newInstance()
    
             //Returns a DefaultGrailsDomainClass (as interface GrailsDomainClass) for inspecting properties
             GrailsClass domainClass = domainInstanceToClone.domainClass.grailsApplication.getDomainClass(newDomainInstance.getClass().name)
    
             def notCloneable = domainClass.getPropertyValue("notCloneable")
    
             for(DefaultGrailsDomainClassProperty prop in domainClass?.getPersistentProperties()) {
                 if (notCloneable && prop.name in notCloneable)
                     continue
    
                 if (prop.association) {
    
                     if (prop.owningSide) {
                         //we have to deep clone owned associations
                         if (prop.oneToOne) {
                             def newAssociationInstance = deepClone(domainInstanceToClone?."${prop.name}")
                             newDomainInstance."${prop.name}" = newAssociationInstance
                         } else {
    
                             domainInstanceToClone."${prop.name}".each { associationInstance ->
                                 def newAssociationInstance = deepClone(associationInstance)
    
                                 if (newAssociationInstance)
                                     newDomainInstance."addTo${prop.name.capitalize()}"(newAssociationInstance)
                             }
                         }
                     } else {
    
                         if (!prop.bidirectional) {
    
                             //If the association isn't owned or the owner, then we can just do a  shallow copy of the reference.
                             newDomainInstance."${prop.name}" = domainInstanceToClone."${prop.name}"
                         }
                         // @@JR
                         // Yes bidirectional and not owning. E.g. clone Report, belongsTo Organisation which hasMany
                         // manyToOne. Just add to the owning objects collection.
                         else {
                             //println "${prop.owningSide} - ${prop.name} - ${prop.oneToMany}"
                             //return
                             if (prop.manyToOne) {
    
                                 newDomainInstance."${prop.name}" = domainInstanceToClone."${prop.name}"
                                 def owningInstance = domainInstanceToClone."${prop.name}"
                                 // Need to find the collection.
                                 String otherSide = prop.otherSide.name.capitalize()
                                 //println otherSide
                                 //owningInstance."addTo${otherSide}"(newDomainInstance)
                             }
                             else if (prop.manyToMany) {
                                 //newDomainInstance."${prop.name}" = [] as Set
    
                                 domainInstanceToClone."${prop.name}".each {
    
                                     //newDomainInstance."${prop.name}".add(it)
                                 }
                             }
    
                             else if (prop.oneToMany) {
                                 domainInstanceToClone."${prop.name}".each { associationInstance ->
                                     def newAssociationInstance = deepClone(associationInstance)
                                     newDomainInstance."addTo${prop.name.capitalize()}"(newAssociationInstance)
                                 }
                             }
                         }
                     }
                 } else {
                     //If the property isn't an association then simply copy the value
                     newDomainInstance."${prop.name}" = domainInstanceToClone."${prop.name}"
    
                     if (prop.name == "dateCreated" || prop.name == "lastUpdated") {
                         newDomainInstance."${prop.name}" = null
                     }
                 }
             }
    
             return newDomainInstance
         }