#StackBounty: #mongodb #hibernate #grails Custom Id Generator for MongoDB not working

Bounty: 50

I have a domain class called Person, for which I’d like to customize the way ID’s are generated. If have a parameter called "fileId" and if this has a value, I want to generate the ID from this, otherwise it should generate a custom ObjectId.

From other threads I’ve seen that it should be possible to create a custom generator by creating a class that implements IdentifierGenerator and override the generate() method. Then in the mapping closure, I specify id generator: "<generator class>", but this doesn’t seem to do anything. I have tried setting breakpoints in the generate() method, but they are never reached. Instead, it seems like grails is instead ignoring my Generator and is using the default method.

I’m using grails version 4.0.10.

My Person class and CustomIdGenerator are defined below:

package hegardt.backend.grails

import grails.mongodb.MongoEntity
import grails.validation.Validateable
import hegardt.backend.grails.enums.Sex
import hegardt.backend.grails.helpers.FormatHelper
import hegardt.backend.grails.helpers.MongoHelper
import hegardt.backend.grails.model.person.LifeMilestone
import hegardt.backend.grails.model.person.Occupation
import hegardt.backend.grails.model.person.Schema
import hegardt.backend.grails.model.person.Spouse
import org.bson.types.ObjectId

import java.time.LocalDateTime

class Person implements MongoEntity<Person>, Schema {

    ObjectId id

    LocalDateTime dateCreated
    LocalDateTime lastUpdated

    String firstName
    List<String> middleNames = []
    String lastName
    Normalized normalized = new Normalized()
    Sex sex = Sex.UNKNOWN
    LifeMilestone birth
    LifeMilestone death
    LifeMilestone burial
    List<Occupation> occupations = []
    String notes
    String fileId
    List<Spouse> spouses = []
    ObjectId father
    ObjectId mother
    List<ObjectId> children = []
    List<String> references = []

    static mapping = {
        id generator: "hegardt.backend.grails.utils.CustomIdGenerator"
        autoTimestamp true
        collection "persons_test"
        database "hegardt"
    }

    static embedded = ['normalized', 'birth', 'death', 'burial', 'occupations', 'spouses']

    static constraints = {
        dateCreated nullable: true
        lastUpdated nullable: true
        firstName nullable: true, blank: false
        middleNames nullable: false, validator: { List<String> val ->
            return noNullValuesInList(val)
        }
        lastName nullable: true, blank: false
        normalized nullable: false, validator: { Normalized val ->
            val.validate()
        }
        sex nullable: false
        birth nullable: true, validator: { LifeMilestone val -> validateNested(val) }
        death nullable: true, validator: { LifeMilestone val -> validateNested(val) }
        burial nullable: true, validator: { LifeMilestone val -> validateNested(val) }
        occupations nullable: false, validator: { List<Occupation> vals ->
            validateNestedList(vals as List<Validateable>)
        }
        notes nullable: true, maxSize: 10000
        fileId nullable: true, blank: false, maxSize: 10, validator: { String val -> val?.isNumber() }
        spouses nullable: false, validator: { List<Spouse> val ->
            return val.every { Spouse s -> s.validate() } && validateUniqueIds(val.collect { Spouse s -> s.id })
        }
        father nullable: true       // TODO Validation by lookup in database
        mother nullable: true       // TODO Validation by lookup in database
        children nullable: false, validator: { List<ObjectId> val ->
            validateUniqueIds(val)
        }
        references nullable: false
    }

    // ------------------------------------------
    // --------------- Getters ------------------
    // ------------------------------------------

    // ... lots of getters ...

    // ------------------------------------------
    // --------------- Events -------------------
    // ------------------------------------------

    def beforeInsert() {
        doBeforeUpdateOrInsert()
        return true
    }

    def beforeUpdate() {
        doBeforeUpdateOrInsert()
    }

    def afterDelete() {
        // 1. Remove this person from parent's children
        Person father = getFatherObject()
        Person mother = getMotherObject()

        father?.children?.remove(id)
        mother?.children?.remove(id)
        father.save()
        mother.save()

        // 2. Remove this person as a parent from children
        List<Person> children = getChildObjects()

        children?.each {
            if (it.father == id) {
                it.father = null
            } else if (it.mother == id) {
                it.mother = null
            }
            it.save()
        }
    }

    private void doBeforeUpdateOrInsert() {
        // Calculate and set normalized fields
        normalized.fullName = FormatHelper.normalizeQueryString(getFullName())

        // Trigger generation of dates
        birth?.generateDate()
        death?.generateDate()
        burial?.generateDate()
        occupations?.each { it.generateDate() }
        spouses?.each { it.generateDate() }

        // Initially, I tried doing this, but the id was simply overwritten with a generated id
        if (fileId) {
            id = MongoHelper.toObjectId(fileId)
        }

        updateReferencedDocuments()
    }

    /**
     * Performs important postprocessing to a Person before it is added to the database. Should be called 'beforeUpdate' and 'beforeInsert'.
     *
     * Adds this person as a parent to all its children as well as a child to this person's parents. For this to work, the person has to have a set gender,
     * otherwise we can not know if it is the mother or the father of its children. Throws an error if it is not set (and has children and is not added already).
     */
    private void updateReferencedDocuments() {
        // 1. Set this person as father/mother of all of its children
        for (Person child in getChildObjects()) {

            if (!(child.father == id) || !(child.mother == id)) { // Not already set
                if (sex == Sex.MAN) {
                    child.father = id
                } else if (sex == Sex.WOMAN) {
                    child.mother = id
                } else {
                    log.error("Person with id ${id} has children specified, but is missing 'sex'.")
                }
            }
            child.save()
        }

        // 2. Set this person as a child to its parents
        Person mother = getMotherObject()
        Person father = getFatherObject()

        if (father && !father.children?.contains(id)) {
            father.children.add(id)
            father.save()
        }
        if (mother && !mother.children?.contains(id)) {
            mother.children.add(id)
            mother.save()
        }
    }
}

class Normalized implements Validateable {
    String fullName

    static constraints = {
        fullName nullable: true, validator: { String val ->
            if (val != null) FormatHelper.isQueryNormalized(val)
        }
    }

}
package hegardt.backend.grails.utils

import org.bson.types.ObjectId
import org.hibernate.HibernateException
import org.hibernate.engine.spi.SharedSessionContractImplementor
import org.hibernate.id.IdentifierGenerator

class CustomIdGenerator implements IdentifierGenerator {

    @Override
    Object generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
        println("GENERATING YOOOOOOOO")  // is never printed and breakpoint is never reached
        return new ObjectId()
    }
}


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.