How do I create and XOR validation for two fields in a Grails domain class?
Asked Answered
O

3

5

I have an issue where my domain class has two potential mutually exclusive external keys, either a serial number or a legacy lookup value.

Since I'm not sure which one I'll have for any given entry, I've made them both nullable and added custom validation to try to ensure I have one and only one value.

package myproject 

class Sample {

    String information
    String legacyLookup
    String serialNumber

    static constraints = {
        information(nullable: true)
        legacyLookup(nullable: true)
        serialNumber(nullable: true)

        legacyLookup validator: {
            return ((serialNumber != null && legacyLookup == null) || (serialNumber == null && legacyLookup != null))
        }

        serialNumber validator: {
            return ((serialNumber != null && legacyLookup == null) || (serialNumber == null && legacyLookup != null))
        }
    }
}

I created the default CRUD screens and tried to create an entry for this domain class

information: Blah Blah
serialNumber: 
legacyLookup: BLAHINDEX123

This dies in the validator with the following message:

No such property: serialNumber for class: myproject.Sample

What am I missing?

Orthodontist answered 14/7, 2012 at 4:20 Comment(0)
G
9

Having each property in there multiple times is not necessary; in fact you only need one of them actually constrained. Also you can't just reference properties directly by their name. There are objects that are passed to the constraint closure that are used to get at the values (see the docs). Probably the simplest way I've found to do this is as follows:

class Sample {
    String information
    String legacyLookup
    String serialNumber

    static constraints = {
        information(nullable: true)
        legacyLookup(validator: {val, obj->
            if( (!val && !obj.serialNumber) || (val && obj.serialNumber) ) {
                return 'must.be.one'
            }
        })
    }
}

And then have an entry in the messages.properties file like this:

must.be.one=Please enter either a serial number or a legacy id - not both

Or you could have separate messages for each condition - both are entered, or both are blank like this:

legacyLookup(validator: {val, obj->
    if(!val && !obj.serialNumber) {
         return 'must.be.one'
    }
    if(val && obj.serialNumber) { 
         return 'only.one'
    }
})

And then have two messages in message.properties:

only.one=Don't fill out both
must.be.one=Fill out at least one...

You don't need to return anything from the constraint if there is no error...

Grindlay answered 14/7, 2012 at 6:20 Comment(1)
This did exactly what I needed. I was thinking that I needed validation explicitly on both fields, but one handles the other. Thanks!Orthodontist
C
0

If you want to ensure you have "one and only one value" another option would be to make a generic field called serialNumberLegacyLookup that would represent both the serialNumber and legacyLookup fields. Then you could add a boolean field to your domain class called legacyLookup which would default to false. You could then run the value through the validator and parse it to see if it was a "serial number" or a "legacy lookup" value. If the value turned out to be a "legacy lookup" value then you would set the legacyLookup boolean to true. I think this approach would be less confusing from a UI perspective (only one field for the user to fill in instead of two conditional fields).

Counterstamp answered 14/7, 2012 at 4:32 Comment(1)
Since the code I'm working with interacts with external legacy systems, I don't think this solution is appropriate. The data that comes to me has one field or the other, but they are modeled as unique fields. There may be some business rules that are unknown to me at this time that may break my understanding of the model. To insulate myself from that possibility, I'm going to keep them separate.Orthodontist
U
-1

I was faced with this same scenario and the solution I found was to create a getter method and add a constraint to it.

package myproject 

class Sample {

    String information
    String legacyLookup
    String serialNumber

    def getTarget(){
        if (legacyLookup && !serialNumber) {
            return legacyLookup
        } else if (!legacyLookup && serialNumber) {
            return serialNumber
        } else {
            return null
        }
    }

    static constraints = {
        information(nullable: true)
        legacyLookup(nullable: true)
        serialNumber(nullable: true)
        target(nullable: false)
    }
}
Unesco answered 22/9, 2017 at 19:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.