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)
}
}
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:
String
, Boolean
, etc via duplicate.properties = original.properties
.HibernateException
about shared collections. So set the collection to null
.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
}
}