I would like to use hibernate search's @IndexingDependency
with a PropertyBridge
but I can't seem to make it work.
I get this error :
Hibernate ORM mapping: type 'com.something.Person': path '.currentStatus':
failures:
- HSEARCH700020: Unable to find the inverse side of the association on type
'com.something.Person' at path '.currentStatus<no value extractors>'. Hibernate Search
needs this information in order to reindex 'com.something.Person' when
'com.something.Status' is modified. You can solve this error by defining the inverse
side of this association, either with annotations specific to your integration
(@OneToMany(mappedBy = ...) in Hibernate ORM) or with the Hibernate Search
@AssociationInverseSide annotation. Alternatively, if you do not need to reindex
'com.something.Person' when 'com.something.Status' is modified, you can disable
automatic reindexing with @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
Not sure if I'm doing something wrong or if what I'm trying to do isn't possible. Thank for the help.
Here are the files involved.
Person.class
@Entity
@Table
@Indexed
public class Person {
@OneToMany(mappedBy = "patient", cascade = CascadeType.ALL)
private Set<Status> status = new HashSet<>();
@Transient
@StatusBinding(fieldName = "currentStatus")
@IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "status")))
public Status getCurrentStatus() {
return this.status.stream()
.filter(it -> it.getDate().isAfter(LocalDate.now()))
.max(Comparator.comparing(Status::getDate))
.orElse(null);
}
}
StatusBinding.class
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef(type = StatusBinding.Processor.class))
@Documented
public @interface StatusBinding {
String fieldName() default "";
class Processor implements PropertyMappingAnnotationProcessor<StatusBinding> {
@Override
public void process(PropertyMappingStep mapping, StatusBindingannotation, PropertyMappingAnnotationProcessorContext context) {
StatusBinderbinder = new StatusBinder();
if (!annotation.fieldName().isBlank()) binder.setFieldName(annotation.fieldName());
mapping.binder(binder);
}
}
}
StatusBinder.class
public class StatusBinder implements PropertyBinder {
@Setter private String fieldName = "mainStatus";
@Override
public void bind(PropertyBindingContext context) {
context.dependencies()
.use("status")
.use("date")
.use("note");
IndexSchemaObjectField mainStatusField = context.indexSchemaElement().objectField(this.fieldName);
context.bridge(Status.class, new StatusBridge(
mainStatusField.toReference(),
mainStatusField.field("status", context.typeFactory().asString()).toReference(),
mainStatusField.field("date", context.typeFactory().asLocalDate()).toReference(),
mainStatusField.field("note", context.typeFactory().asString()).toReference()
));
}
private static class StatusBrige implements PropertyBridge<Status> {
private final IndexObjectFieldReference mainStatusField;
private final IndexFieldReference<String> statusField;
private final IndexFieldReference<LocalDate> dateField;
private final IndexFieldReference<String> noteField;
public StatusBrige(
IndexObjectFieldReference mainStatusField,
IndexFieldReference<String> statusField,
IndexFieldReference<LocalDate> dateField,
IndexFieldReference<String> noteField
) {
this.mainStatusField = mainStatusField;
this.statusField = statusField;
this.dateField = dateField;
this.noteField = noteField;
}
@Override
public void write(DocumentElement target, Status mainStatus, PropertyBridgeWriteContext context) {
DocumentElement statutElement = target.addObject(this.mainStatusField);
statutElement.addValue(this.statusField, mainStatus.getStatus);
statutElement.addValue(this.dateField, mainStatus.getDate());
statutElement.addValue(this.noteField, mainStatus.getNote());
}
}
}
When a Status
entity is modified, Hibernate Search doesn't know how to retrieve the corresponding Person
having that Status
as its currentStatus
.
Assuming the currentStatus
is always contained in status
, and since Status.patient
is the inverse side of the Person.status
association, you should only need to add this:
@Transient
@StatusBinding(fieldName = "currentStatus")
@IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "status")))
// ADD THIS ANNOTATION
@AssociationInverseSide(
inversePath = @ObjectPath(@PropertyValue(propertyName = "patient"))
)
public Status getCurrentStatus() {
// ...
}
I'll try to explain this, but it's a bit complex, so bear with me.
Derived properties and the inverse side of associations are related concepts: they share the common purpose of allowing Hibernate Search to perform automatic reindexing.
However, they are still separate concepts, and Hibernate Search is not able to infer one from the other.
With @IndexingDependency(derivedFrom)
, you are defining what the computation of currentStatus
depends on:
@IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "status")))
public Status getCurrentStatus() {
This tells Hibernate Search that currentStatus
will change whenever the status
property changes. With that information, Hibernate Search is able to determine that whenever you call person.getStatus().remove(...)
or person.getStatus().add(...)
(for example), your Person
entity needs reindexing, because currentStatus
is indexed, and it probably changed.
In your custom binder, you're also defining dependencies:
context.dependencies()
.use("status")
.use("date")
.use("note");
This tells Hibernate Search that whenever the status
, date
, and note
properties of a Status
entity change, the Person
having that Status
as its currentStatus
will need reindexing.
However... what Hibernate Search doesn't know is how to retrieve the person having that Status
as its currentStatus
.
It may know how to retrieve all persons having that Status
in their status
set, but that's a different thing, isn't it? Hibernate Search doesn't know that currentStatus
is actually one of the elements contained in the status
property. For all it knows, getCurrentStatus()
could very well be doing this: status.iterator().next().getParentStatus()
. Then the current status wouldn't be included in Person#status
, and it's unclear if myStatus.getPatient()
could return a Person
whose currentStatus
is myStatus
.
So you need to tell Hibernate Search explicitly: "from a given Status myStatus
, if you retrieve the value of myStatus.getPatient()
, you get the Person
whose currentStatus
property may point back to myStatus
". That's exactly what @AssociationInverseSide
is for.