#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()
затем не имеет никакого эффекта :/ См. Мой обновленный вопрос.