Search code examples
grailsgrails-ormgrails-2.0grails-domain-class

Cannot change domain class property value in Grails


I am developing a Grails 2.3.7 application and I'm having trouble changing a domain property with a select box. Every time I try to change the property and save, I get a HibernateException: identifier of an instance of Ethnicity was altered from X to Y. I don't want to change the ID of the ethnicity, I simply want to change the ApplicationPersons ethnicity from one to another.

A few things to note:

  1. I am using the same controller action to create AND update the person.
  2. Setting personInstance.ethnicity to null right before personInstance.properties = params will make the save work, but I don't know why, and I don't want to do this for every association that I want to change.
  3. I realize the domain model seems odd. It is a legacy DB that I cannot change.

Here are my domain classes:

class ApplicationPerson implements Serializable {
    Integer appId
    Integer applicationSequenceNumber
    String firstName
    String lastName
    Ethnicity ethnicity

    static mapping = {
        id composite: ['appId', 'applicationSequenceNumber'],
           generator: 'assigned'
    }
}

class Ethnicity {
    String code
    String description

    static mapping = {
        id name: 'code', generator: 'assigned'
    }
}

Here is my _form.gsp to update the Ethnicity (I removed all the other properties that are saving just fine):

<div class="fieldcontain ${hasErrors(bean: personInstance, 
                                     field: 'ethnicity', 'error')} ">
    <label for="ethnicity">Ethnicity</label>
    <g:select id="ethnicity" 
              name="ethnicity.code" 
              from="${Ethnicity.list()}" 
              optionKey="code" 
              value="${personInstance?.ethnicity?.code}" />
</div>

And lastly, my controller action that the form POSTs to:

def save() {
    Application app = applicationService.getCurrentApplication()

    // Find/Create and save Person
    ApplicationPerson personInstance = app.person
    if (!personInstance) {
        personInstance = 
            new ApplicationPerson(appId: app.id, 
                                  applicationSequenceNumber: app.sequenceNumber)
    }
    personInstance.properties = params

    if (!personInstance.validate()) {
        respond personInstance.errors, view:'edit'
        return
    }

    personInstance.save flush:true
    redirect action: 'list'
}

Solution

  • Modify the name in the select element from ethnicity.code to personInstance.etnicity.code as shown below:

    <g:select id="ethnicity" 
              name="personInstance.ethnicity.code" 
              from="${Ethnicity.list()}" 
              optionKey="code" 
              value="${personInstance?.ethnicity?.code}" />
    

    The name of selected option gets bound to params as key and the selected value as the value against the key. Using ethnicity.code would try to modify the primary key of an existing ethnicity instead of modifying the ethnicity of an application person.

    UPDATE
    Above change in name is optional (can be used in case you don't need params to be assigned as properties of domain class). Previous name ethnicity.code should work as well but below changes are also required in the controller action in order to set ethnicity:

    //or use params.ethnicity.code
    //if name is ethnicity.code
    person.ethnicity = Ethnicity.load(params.personInstance.ethnicity.code)
    
    //or use params if name is ethnicity.code
    person.properties = params.personInstance
    

    Load an existing Ethnicity based on the passed in code and then set it in person before setting properties.

    The issue lies with code being the primary key of Ethnicity. If Ethnicity had a separate identity column (for example, the default Long id) then your exact implementation in question would work with the help of data binding. But since it is mentioned that you are working with legacy database, I suppose you won't be able to modify the tables to add another column for id. So your best bet will be to load(cheap compared to get, as row is loaded from hibernate cache) ethnicity from the passed in code and then set it to person.

    You can also see try caching the Ethnicity domain if possible because that will be master data and a good candidate for caching.