Search code examples
hibernate-searchhibernate-search-6

Using @IndexingDependency derivedFrom and a property bridge


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());
        }
    }
}

Solution

  • Problem

    When a Status entity is modified, Hibernate Search doesn't know how to retrieve the corresponding Person having that Status as its currentStatus.

    Solution

    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() {
           // ...
        }
    

    Why?

    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.