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复制所有“原始”属性,例如StringBoolean等。 由于上面还设置了集合/具有多关系,这将导致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 中一对多域的深拷贝的主要内容,如果未能解决你的问题,请参考以下文章

Python中的深拷贝和浅拷贝

Python中的深拷贝和浅拷贝

数组的深拷贝

python的深拷贝与浅拷贝

[随笔重写] Python3 的深拷贝与浅拷贝

python 的深拷贝与浅拷贝