Пользовательский генератор идентификаторов для MongoDB не работает

#mongodb #hibernate #grails

Вопрос:

У меня есть доменный класс Person, для которого я хотел бы настроить способ генерации идентификаторов. Если у вас есть параметр с именем «fileId» и если у него есть значение, я хочу сгенерировать идентификатор на основе этого, в противном случае он должен сгенерировать пользовательский идентификатор объекта.

Из других потоков я видел, что должно быть возможно создать пользовательский генератор, создав класс, который реализует IdentifierGenerator и переопределяет generate() метод. Затем в закрытии сопоставления я указываю id generator: "<generator class>" , но , похоже, это ничего не дает. Я пытался установить точки останова в generate() методе, но они так и не были достигнуты. Вместо этого кажется, что grails игнорирует мой генератор и использует метод по умолчанию.

Я использую grails версии 4.0.10.

Мой Person класс и CustomIdGenerator определены ниже:

 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() } amp;amp; 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 amp;amp; !father.children?.contains(id)) {
            father.children.add(id)
            father.save()
        }
        if (mother amp;amp; !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()
    }
}
 

Update 1

I have also tried what I thought should be a simple solution; using «assigned» and assigning in beforeInsert() . For some reason, this just makes the insert() or save() functions not do anything. If I explicitly set an id on the instance, like person.id = new ObjectId() in my test, then it works. Changed parts:

Person.groovy

 static mapping = {
        id generator: "assigned"
        autoTimestamp true
        collection "persons_test"
        database "hegardt"
    }

def beforeInsert() {
        if (fileId) {
            id = MongoHelper.toObjectId(fileId)
        } else {
            id = new ObjectId()
        }
        return true
    }
 

Сбой модульного теста:

 void "test"() {
        given:
            Person p = PersonTestHelper.getPerson()
            // p.id = new ObjectId() <-- WORKS, but obv. not a solution
        expect:
            !p.id
        when:
            Person savedPerson = p.insert(flush: true, failOnError: true)  // Returns p unsaved
        then:
            Person.count() == 1  // FAILS
    }
 

Обновление 2

Так что еще более странно то, что я попробовал стратегию «назначено», запустив приложение и отправив человека через почтальона, и теперь мой крючок «beforeInsert ()» вообще не вызывается, и я получаю подобную ошибку при вызове save() или insert() : Entity [hegardt.backend.grails.Person : (unsaved)] has null identifier when identifier strategy is manual assignment. Assign an appropriate identifier before persisting.. Stacktrace follows:

Комментарии:

1. У меня нет места, чтобы проверить это, поэтому я публикую это только в качестве отправной точки для вас. Согласно документам grails (которые не всегда идеальны), вы не можете указать здесь класс. Однако вы можете указать «назначено». Будет ли тогда работать объединение id: 'assigned' и beforeInsert присвоение значения полю?

2. Уточнение, так как уже слишком поздно редактировать предыдущий комментарий: id generator: 'assigned'

3. Привет, @Daniel, спасибо за ответ. Я также не могу найти никаких ссылок на использование реализации пользовательского генератора в документах, но нашел по крайней мере 4 потока, так что это должно быть возможно. Что касается использования «назначено», я пробовал это, но вызов save() или insert() затем не имеет никакого эффекта :/ См. Мой обновленный вопрос.