3

I'm confused as to how to implement this or if it's really even possible/appropriate. My colleague and I are building a web app for a client using Grails 3. He created the initial domains which I'm guessing where an almost one-to-one copy from the Realm models from the mobile apps. I've since modified them in an attempt to get some form of deep cloning to work as three domains have a one-to-many relationship.

The Problem

How would I go about creating a deep copy of a domain? I have tried suggested answers with little success:

Picking ideas from various places I've come to formulating a clone(Domain) method shown below. It almost works (I think), but has issues with the collections throwing a HibernateException - Found shared references to a collection: Location.equipments.

Called in a controller as:

def copy() {
    Survey.clone(Survey.get(params.id))
    redirect action: 'index'
}

Any ideas or guidance?

Currently the domains are the following:

class Survey {

    int id
    String name
    String contactName
    String contactEmail
    String facilityAddress
    String facilityCity
    String facilityStateProvince
    String facilityZip
    String distributorName
    String distributorEmail
    String distributorPhoneNumber

    static Survey clone(Survey self) {
        Survey clone = new Survey()
        String exclude = "locations"

        clone.properties = self.properties.findAll {
            it.key != exclude
        }

        self.locations.each {
            Location copy = Location.clone it
            clone.addToLocations copy
        }

        clone.save()
    }

    static transients = ['clone']
    static belongsTo = User
    static hasMany = [locations: Location]
}

class Location {
    int id
    String name
    String[] hazardsPresent
    HazardType[] hazardTypes
    ExposureArea[] exposureArea
    RiskLevel exposureLevel
    String comments
    byte[] picture

    static Location clone(Location self) {
        Location clone = new Location()
        String[] excludes = ['equipment', 'products']

        clone.properties = self.properties.findAll {
            !(it.key in excludes)
        }

        self.equipments.each {
            Equipment copy = Equipment.clone it
            self.addToEquipments copy
        }

        self.products.each {
            RecommendedProduct copy = new RecommendedProduct()
            copy.properties = it.properties
            copy.save()
            clone.addToProducts copy
        }

        clone.save()
    }

    static transients = ['clone']
    static belongsTo = Survey
    static hasMany = [equipments: Equipment, products: RecommendedProduct]
    static constraints = {
        picture(maxSize: 1024 * 1024)
    }
}

class Equipment {
    int id
    EquipmentType type
    String name
    Brand brand

   // Redacted 26 boolean properties
   // ...

    static Equipment clone(Equipment self) {
        Equipment clone = new Equipment()
        String exclude = "extras"

        clone.properties = self.properties.findAll {
            it.key != exclude
        }

        self.extras.each {
            EquipmentQuestionExtra copy = new EquipmentQuestionExtra()
            copy.properties = it.properties
            copy.save()
            clone.addToExtras copy
        }

        clone.save()
    }

    static transients = ['clone']
    static belongsTo = Location
    static hasMany = [extras: EquipmentQuestionExtra]
}

class RecommendedProduct {
    int productId
    int quantityChosen
    String comment

    static belongsTo = Location
}

class EquipmentQuestionExtra {
    int id
    String questionText
    String comment
    byte[] picture

    static belongsTo = Equipment
    static constraints = {
        picture(maxSize: 1024 * 1024)
    }
}
Community
  • 1
  • 1
Cisco
  • 20,972
  • 5
  • 38
  • 60

3 Answers3

1

There may be a problem with your clone method: you clone "all" properties, including the ID, which is a bad idea with deep cloning. This thread explanes that your error is thrown when an object has the same properties than another in the hibernate cache, BUT with another reference.

So, you just have to set the id property of your objects to null (or exclude it from the properties copy) to force hibernate to detect that it's a new object. If it's still not working, call the discard method on your object before the save.

Community
  • 1
  • 1
Joch
  • 230
  • 1
  • 9
1

It's been almost a year and I've since completed this project with a solution to this problem.

The solution I came up with was utilizing the service layer. I defined a service for each domain. Any domain that needed to deep copy a collection, called their associated service method. I'm only posting the source of two services as the other methods are essentially the same.

The flow is this:

  1. Create a new blank instance of the domain.
  2. Copy all 'primitive' properties such as String, Boolean, etc via duplicate.properties = original.properties.
  3. Since the above also sets the collection/has-many relationships, this would result in a HibernateException about shared collections. So set the collection to null.
  4. Call the associated service method to copy the collection/has-many.
  5. Save and return the duplicated domain.

service/SurveyService.groovy

class SurveyService {
/**
 * Attempts to perform a deep copy of a given survey
 *
 * @param survey The survey instance to duplicate
 * @return The duplicated survey instance
 */
Survey duplicateSurvey(Survey originalSurvey) {
    Survey duplicatedSurvey = new Survey()

    duplicatedSurvey.properties = originalSurvey.properties
    duplicatedSurvey.locations = null
    duplicatedSurvey.uuid = UUIDGenerator.createUniqueId()
    duplicatedSurvey.dateModified = DateUtil.getCurrentDate()
    duplicatedSurvey.name = "${originalSurvey.name.replace("(copy)", "").trim()} (copy)"
    duplicatedSurvey.save()
    duplicatedSurvey.locations = duplicateLocations originalSurvey.locations, duplicatedSurvey
    duplicatedSurvey.save()
}

/**
 * Attempts to perform a deep copy of a survey's location
 *
 * @param originalLocations The original location set
 * @param duplicatedSurvey The duplicated survey that each survey will belong to
 * @return The duplicated location set
 */
Set<Location> duplicateLocations(Set<Location> originalLocations, Survey duplicatedSurvey) {
    Set<Location> duplicatedLocations = []

    for (originalLocation in originalLocations) {
        duplicatedLocations << locationService.duplicateLocation(originalLocation, duplicatedSurvey)
    }

    duplicatedLocations
}
}

service/LocationService.groovy

class LocationService {
    /**
     * Performs a deep copy of a given location. The duplicated location name is
     * the original location name and the duplicated location ID.
     *
     * @param originalLocation The location to duplicate
     * @param survey The survey that the location will belong to
     * @return The duplicated location
     */
    Location duplicateLocation(Location originalLocation, Survey survey = null) {
        Location duplicatedLocation = new Location()
        duplicatedLocation.properties = originalLocation.properties
        duplicatedLocation.survey = survey ?: duplicatedLocation.survey
        duplicatedLocation.uuid = UUIDGenerator.createUniqueId()
        duplicatedLocation.dateModified = DateUtil.currentDate
        duplicatedLocation.equipments = null
        duplicatedLocation.products = null
        duplicatedLocation.save()
        duplicatedLocation.name = "${originalLocation.name.replace("(copy)", "").trim()} (copy)"
        duplicatedLocation.equipments = duplicateEquipment originalLocation.equipments, duplicatedLocation
        duplicatedLocation.products = duplicateProducts originalLocation, duplicatedLocation
        duplicatedLocation.save()

        duplicatedLocation
    }

    /**
     * Performs a deep copy of a given locations equipments.
     *
     * @param originalEquipments The original locations equipments
     * @param duplicatedLocation The duplicated location; needed for belongsTo association
     * @return The duplicated equipment set.
     */
    Set<Equipment> duplicateEquipment(Set<Equipment> originalEquipments, Location duplicatedLocation) {
        Set<Equipment> duplicatedEquipments = []

        for (originalEquipment in originalEquipments) {
            Equipment duplicatedEquipment = new Equipment()
            duplicatedEquipment.properties = originalEquipment.properties
            duplicatedEquipment.uuid = UUIDGenerator.createUniqueId()
            duplicatedEquipment.dateModified = DateUtil.currentDate
            duplicatedEquipment.location = duplicatedLocation
            duplicatedEquipment.extras = null
            duplicatedEquipment.save()
            duplicatedEquipment.name = "${originalEquipment.name.replace("(copy)", "").trim()} (copy)"
            duplicatedEquipment.extras = duplicateExtras originalEquipment.extras, duplicatedEquipment
            duplicatedEquipments << duplicatedEquipment
        }

        duplicatedEquipments
    }

    /**
     * Performs a deep copy of a given locations extras.
     *
     * @param originalExtras The original location extras
     * @param duplicatedEquipment The duplicated equipment; needed for belongsTo association
     * @return The duplicated extras set.
     */
    Set<EquipmentQuestionExtra> duplicateExtras(Set<EquipmentQuestionExtra> originalExtras, Equipment duplicatedEquipment) {
        Set<EquipmentQuestionExtra> duplicatedExtras = []

        for (originalExtra in originalExtras) {
            EquipmentQuestionExtra duplicatedExtra = new EquipmentQuestionExtra()
            duplicatedExtra.properties = originalExtra.properties
            duplicatedExtra.equipment = duplicatedEquipment
            duplicatedExtra.uuid = UUIDGenerator.createUniqueId()
            duplicatedExtra.dateModified = DateUtil.currentDate
            duplicatedExtra.save()
            duplicatedExtras << duplicatedExtra
        }

        duplicatedExtras
    }

    /**
     * Performs a deep copy of a given locations products.
     *
     * @param originalLocation The original location
     * @param duplicatedLocation The duplicated location
     * @return The duplicated product set.
     */
    Set<RecommendedProduct> duplicateProducts(Location originalLocation, Location duplicatedLocation) {
        Set<RecommendedProduct> originalProducts = originalLocation.products
        Set<RecommendedProduct> duplicatedProducts = []

        for (originalProduct in originalProducts) {
            RecommendedProduct duplicatedProduct = new RecommendedProduct()
            duplicatedProduct.properties = originalProduct.properties
            duplicatedProduct.location = duplicatedLocation
            duplicatedProduct.uuid = UUIDGenerator.createUniqueId()
            duplicatedProduct.dateModified = DateUtil.currentDate
            duplicatedProduct.save()
            duplicatedProducts << duplicatedProduct
        }

        duplicatedProducts
    }
}
Cisco
  • 20,972
  • 5
  • 38
  • 60
0

You are saving the children entities. Dont do that, only save the entity Survey (root). Others will be saved by cascade.

In other hand as @Joch says, using clone is not the right approach in this case.

You should create a duplicate method of you entity. Here is an example how to clone a this structure type. It is a test with n questions, and each question has n answers, with a "duplicate" method for each class.

class Test {

    String name

    static hasMany = [
            /**
             * Each question of the test
             */
            questions: Question
    ]

    /**
     * Duplicates this test
     */
    Test duplicate(){
         Test test = new Test(name:this.name)
         this.questions.each{ question ->
             test.addToQuestions(question.duplicate(test))
         }
         test
    }
}


class Question {

    Integer questionOrder
    String title

    /**
     * Each question belong to a Test
     */
    static belongsTo = [test:Test]

    static hasMany = [
            /**
             * Each answer of the test
             */
            answers: Answer
    ]

    /**
     * Duplicates this test to another edition
     * @param edition to be duplicated
     * @return duplicated question
     */
    Question duplicate(Test test){
        if(test){
            Question question = new Question(title:this.title)
            this.answers.each{ answer->
                question.addToAnswers(answer.duplicate())
            }
            test.addToQuestions(question)
            question
        }
    }
}

class Answer {

    String title
    boolean correct
    /**
     * Each answer belongs to a question
     */
    static belongsTo = [question:Question]

    /**
     * Duplicates this answer to another question
     * @param edition to be duplicated
     * @return duplicated question
     */
    Answer duplicate(){
        Answer answer = new Answer()
        answer.properties['title','correct'] = this.properties['title','answerOrder','correct']
        answer
    }
}

In Answer.duplicate() you have an example of how to bind certain properties from other object.

quindimildev
  • 1,280
  • 8
  • 21
  • What's the purpose of checking for null with the `if` block? It doesn't make sense to me since it's the only method with the signature `duplication(Domain)`. I've been playing around with your suggestions, however it's throwing null exceptions on the collections. – Cisco Aug 26 '16 at 15:21
  • Sorry I cleaned a real code and I have forgotten to delete that line. – quindimildev Aug 29 '16 at 05:57
  • I have seen you are saving the children entities. Dont do that, only save the entity Survey. Others will be saved by cascade – quindimildev Aug 29 '16 at 06:03