Grails 3 中一对多域的深拷贝
Posted
技术标签:
【中文标题】Grails 3 中一对多域的深拷贝【英文标题】:Deep copy of one-to-many domains in Grails 3 【发布时间】:2017-01-02 14:26:29 【问题描述】:我很困惑如何实现这一点,或者它是否真的可能/合适。我和我的同事正在使用 Grails 3 为客户端构建一个 Web 应用程序。他创建了初始域,我猜想这是来自移动应用程序的 Realm 模型的几乎一对一的副本。从那以后,我对它们进行了修改,试图让某种形式的深度克隆能够发挥作用,因为三个域具有一对多的关系。
问题
我将如何创建域的深层副本?我尝试了建议的答案,但收效甚微:
Proper Implementation of clone() For Domain Classes to duplicate a Grails domain instance
can GORM duplicate whole object?
Cloning an instance of domain in Grails
从不同的地方挑选想法我来制定如下所示的clone(Domain)
方法。它几乎可以工作(我认为),但在集合抛出 HibernateException - Found shared references to a collection: Location.equipments
时存在问题。
在控制器中调用为:
def copy()
Survey.clone(Survey.get(params.id))
redirect action: 'index'
有什么想法或指导吗?
目前的域名如下:
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)
【问题讨论】:
您正在进行的克隆应该是克隆每个对象,而不管其类型如何。因此,也要克隆集合。 【参考方案1】:您的克隆方法可能存在问题:您克隆了“所有”属性,包括 ID,这对于深度克隆来说是个坏主意。 This thread 说明当一个对象与休眠缓存中的另一个对象具有相同的属性时,您的错误将被抛出,但具有另一个引用。
因此,您只需将对象的id
属性设置为null
(或将其从属性副本中排除)以强制休眠检测它是一个新对象。如果仍然无法正常工作,请在 save
之前对您的对象调用 discard 方法。
【讨论】:
【参考方案2】:已经快一年了,我已经完成了这个项目并解决了这个问题。
我想出的解决方案是使用service layer。我为每个域定义了一个服务。任何需要深度复制集合的域,称为其关联的服务方法。我只是发布两个服务的来源,因为其他方法基本相同。
流程是这样的:
-
创建域的新空白实例。
通过
duplicate.properties = original.properties
复制所有“原始”属性,例如String
、Boolean
等。
由于上面还设置了集合/具有多关系,这将导致HibernateException
关于共享集合。所以将集合设置为null
。
调用关联的服务方法复制collection/has-many。
保存并返回重复的域。
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
【讨论】:
为我工作。非常感谢,我在这个问题上浪费了大约半天时间。【参考方案3】:您正在保存子实体。不要那样做,只保存实体调查(根)。其他的会被级联保存。
另一方面,正如@Joch 所说,在这种情况下使用克隆不是正确的方法。
您应该为您的实体创建一个重复的方法。这是一个如何克隆这种结构类型的示例。这是一个有n个问题的测试,每个问题有n个答案,每个班级都有一个“重复”的方法。
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
在 Answer.duplicate() 中有一个示例,说明如何绑定来自其他对象的某些属性。
【讨论】:
使用if
块检查 null 的目的是什么?这对我来说没有意义,因为它是唯一带有签名duplication(Domain)
的方法。我一直在考虑你的建议,但是它在集合上抛出了 null 异常。
对不起,我清理了一个真实的代码,我忘了删除那行。
我看到您正在保存子实体。不要那样做,只保存实体调查。其他的会被级联保存以上是关于Grails 3 中一对多域的深拷贝的主要内容,如果未能解决你的问题,请参考以下文章