Use ID bridges from the mapper when building ID predicates

Description

Follow-up on HSEARCH-3088, where we added support for ID predicates, but only using the String type used in the backend, without taking into account the fact that users usually manipulate a different type for their ID (Long, Integer, ...) that is transparently converted by the mapper to the String required by the backend.

We already tackled a similar problem for the "match" predicate: the org.hibernate.search.engine.search.predicate.spi.MatchPredicateBuilder#value accepts arguments of the type used in the mapped entity (say, MyEnum) while the index manager may internally manipulate another type (for enums, that internal type would be String).

This was solved in https://hibernate.atlassian.net/browse/HSEARCH-3221#icft=HSEARCH-3221 by requiring the mapper to pass a "converter" to the index manager at bootstrap, so that the index manager can later use that converter in its implementation of the predicate DSL, whenever the user passes a value.

So, we only have to do the same for IDs.

First step: alter the backends so that they accept such a converter from the mapper:

  1. Introduce a ToIndexIdValueConverter interface, similar to ToIndexFieldValueConverter, with its own ToIndexIdValueConvertContext. Note that there will only be one generic parameter in ToIndexIdValueConverter, since the ID is always a String in the backend.

  2. Introduce a way for the mapper to pass the converter when building the index manager. I think the best place to do this would be a method in org.hibernate.search.engine.mapper.mapping.building.spi.IndexModelBindingContext, next to explicitRouting(). Say, void idConverter(ToIndexIdValueConverter). As to implementations:

    1. In NonRootIndexModelBindingContext, as with explicitRouting(), you can throw an assertion failure, since it should never be called.

    2. In RootIndexModelBindingContext, you should delegate to the IndexSchemaRootNodeBuilder, via a newly introduce method in that inteface (say void idConverter(ToIndexIdValueConverter) too). For now keep the implementation of that method in ElasticsearchIndexSchemaRootNodeBuilder, LuceneIndexSchemaRootNodeBuilder and StubIndexSchemaRootNodeBuilder very simple: just store the converter in a field, even if it's not used.

  3. Make sure the converter is stored in the data that will be used at runtime. It's a bit complex, but I think your best bet will be to:

    1. Rename the interface ElasticsearchRootIndexSchemaContributor to ElasticsearchIndexModelBuilder (and similarly in other backends)

    2. Change the method RootTypeMapping contribute(ElasticsearchIndexSchemaNodeCollector collector); and replace it with a method ElasticsearchIndexModel build(String hibernateSearchIndexName, URLEncodedString elasticsearchIndexName, ElasticsearchIndexSettingsBuilder settingsBuilder) (and similarly in other backends); the implementation will take care of calling new ElasticsearchIndexModel with all the appropriate arguments (including the map of object nodes and field nodes) and will return the model.

    3. Add a parameter to the ElasticsearchIndexModel constructor: the ID converter. Pass it as necessary where the constructor is called. Add a "idConverter" attribute in ElasticsearchIndexModel as well as a getter.

  4. Actually use the converter at runtime

    1. Add a getIdConverter() method to ElasticsearchSearchTargetModel. The implementation will be similar to getSchemaNodeComponent, but simpler: essentially you need to get the idConverter of the first index model, check that it's compatible with the converter of all other index models, and if so return it.

    2. Use that getIdConverter method in org.hibernate.search.backend.elasticsearch.search.predicate.impl.SearchPredicateBuilderFactoryImpl#id and pass it to the MatchIdPredicateBuilderImpl constructor.

That should be it. Now you can add a test next to the existing MatchIdPredicateIT, except in that case you will call ctx.idConverter(someConverter) in the ctx -> ... lambda in the setup method of your test. You can use org.hibernate.search.integrationtest.backend.tck.util.ValueWrapper<String> to easily simulate a type to be converted to/from String.

Second step: actually implement that in the POJO mapper:

  1. Create an implementation of ToIndexIdValueConverter that delegates to a IdentifierBridge. See org.hibernate.search.mapper.pojo.mapping.building.impl.ValueBridgeToIndexFieldValueConverter, it should be fairly similar, except that instead of delegating to a ValueBridge, it will delegate to a IdentifierBridge. You will probably need to add a isCompatibleWith(IdentifierBridge) method to IdentifierBridge: again, you can take inspiration from org.hibernate.search.mapper.pojo.bridge.ValueBridge#isCompatibleWith

  2. Change org.hibernate.search.mapper.pojo.mapping.building.impl.PojoIndexModelBinderImpl#addIdentifierBridge to:

    1. Pass a IndexModelBindingContext bindingContext parameter, much like we do in org.hibernate.search.mapper.pojo.mapping.building.impl.PojoIndexModelBinderImpl#addRoutingKeyBridge

    2. Call bindingContext.idConverter( new IdenfitierBridgeToIndexIdValueConverter( bridge ) ), just before returning from the method.

Activity

Show:
Fixed

Details

Assignee

Reporter

Sprint

Fix versions

Priority

Created November 16, 2018 at 9:38 AM
Updated January 31, 2019 at 3:08 PM
Resolved December 13, 2018 at 9:02 AM