Search code examples
grailsgrails-orm

Deep copy of one-to-many domains in Grails 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)
    }
}

Solution

  • 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
        }
    }