Skip to content

Commit

Permalink
HSEARCH-3709 Document routing key bridges
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Jul 9, 2020
1 parent 8eac163 commit 250ff8c
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 48 deletions.
Expand Up @@ -3,27 +3,123 @@

== Basics

include::todo-placeholder.asciidoc[]
//TODO HSEARCH-3709 basic example with annotation, binder, bridge, mapping
include::components/sharding-intro-note.asciidoc[]

A routing key bridge is a pluggable component that implements
the mapping of an entity type to a routing key,
which will determine <<concepts-sharding-routing,to which shard the corresponding indexed document will be routed to>>.
It is applied to a type with the `@RoutingKeyBinding` annotation
or with a <<mapper-orm-custom-annotations,custom annotation>>.

Implementing a routing key bridge requires two components:

. A custom implementation of `RoutingKeyBinder`, to bind the bridge to a type at bootstrap.
This involves declaring the properties of the type that will be used
and instantiating the type bridge.
. A custom implementation of `RoutingKeyBridge`, to generate a routing key at runtime.
This involves extracting data from an instance of the type, transforming the data if necessary,
and returning the routing key.

Below is an example of a custom routing key bridge that
uses the `genre` property of the `Book` class as a routing key.

.Implementing and using a `RoutingKeyBridge`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/routingkeybridge/simple/BookGenreRoutingKeyBinder.java[tags=binder]
----
<1> The binder must implement the `RoutingKeyBinder` interface.
<2> Implement the `bind` method in the binder.
<3> Declare the dependencies of the bridge,
i.e. the parts of the type instances that the bridge will actually use.
See <<mapper-orm-bridge-bridgedelement-dependencies>>
for more information about declaring dependencies.
<4> Call `context.bridge(...)` to define the type bridge to use,
and pass an instance of the bridge.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/routingkeybridge/simple/BookGenreRoutingKeyBinder.java[tags=bridge]
----
<1> The bridge must implement the `RoutingKeyBridge` interface.
Here the bridge class is nested in the binder class,
because it is more convenient,
but you are obviously free to implement it in a separate java file.
<2> Implement the `toRoutingKey` method in the bridge.
This method is called on indexing.
<3> The bridged element is passed as an `Object`,
so cast it to the correct type.
<4> Extract data from the bridged element,
and optionally transform it,
to generate the routing key.
<5> Return the routing key.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/routingkeybridge/simple/Book.java[tags=include;!getters-setters]
----
<1> Apply the bridge using the `@RoutingKeyBridge` annotation.
<2> Properties used in the bridge can still be mapped as index fields.
====

See <<search-dsl-query-routing>> for an example of how to use routing in search queries, with the same mapping as above.

[[mapper-orm-bridge-routingkeybridge-parameters]]
== Passing parameters

include::todo-placeholder.asciidoc[]
//TODO HSEARCH-3709 parameterized example
Routing key bridges are usually applied with the built-in `@RoutingKeyBinding` annotation.
which does not accept any parameter other than the routing key binder.

In some cases, it is necessary to pass parameters directly to the routing key binder.
This is achieved by defining a <<mapper-orm-custom-annotations,custom annotation>> with attributes.

Refer to <<mapper-orm-bridge-typebridge-parameters,this example for `TypeBinder`>>,
which is fairly similar to what you'll need for a `RoutingKeyBinder`.

== Accessing the ORM session from the bridge

include::todo-placeholder.asciidoc[]
// TODO HSEARCH-3709 HibernateOrmExtension.get()? Make sure to warn that not all operations are valid.
Contexts passed to the bridge methods can be used to retrieve the Hibernate ORM session.

.Retrieving the ORM session from a `RoutingKeyBridge`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/routingkeybridge/ormcontext/MyEntityRoutingKeyBinder.java[tags=include]
----
<1> Apply an extension to the context to access content specific to Hibernate ORM.
<2> Retrieve the `Session` from the extended context.
====

== Injecting beans into the binder

include::todo-placeholder.asciidoc[]
// TODO HSEARCH-3709 say it's supported, give some basic information and link to <<configuration-bean-injection>>
With <<configuration-bean-frameworks,compatible frameworks>>,
Hibernate Search supports injecting beans into:

* the `TypeMappingAnnotationProcessor` if you use custom annotations and instantiate the binder yourself.
* the `RoutingKeyBinder` if you use the `@RoutingKeyBinding` annotation and let Hibernate Search
instantiate the binder using your dependency injection framework.

NOTE: This only applies to binders instantiated by Hibernate Search itself.
As a rule of thumb, if you need to call `new MyBinder()` at some point,
the binder won't get auto-magically injected.

The context passed to the type binder's `bind` method
also exposes a `getBeanResolver` method to access the bean resolver and instantiate beans explicitly.

See <<configuration-bean-injection>> for more details.

== Incubating features

include::components/incubating-warning.asciidoc[]

include::todo-placeholder.asciidoc[]
// TODO HSEARCH-3709 incubating support for reflection with bridgedElement() (advanced use, no example)
The context passed to the routing key binder's `bind` method
exposes a `bridgedElement()` method that gives access to metadata about the type being bound.

The metadata can in particular be used to inspect the type in details:

* Getting accessors to properties.
* Detecting properties with markers.
Markers are applied by specific annotations carrying a `@MarkerBinding` meta-annotation.

See the javadoc for more information.
Expand Up @@ -284,7 +284,7 @@ when building the query:
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/routing/HibernateOrmRoutingIT.java[tags=routing-single]
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/routingkeybridge/simple/RoutingKeyBridgeSimpleIT.java[tags=routing-single]
----
<1> Start building the query.
<2> Define that only documents matching the given `genre` should be returned.
Expand Down
@@ -0,0 +1,14 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.bridge.routingkeybridge.ormcontext;

public enum MyData {

INDEXED,
PROJECTED;

}
@@ -0,0 +1,29 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.bridge.routingkeybridge.ormcontext;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.RoutingKeyBinderRef;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.RoutingKeyBinding;

@Entity
@Indexed
@RoutingKeyBinding(binder = @RoutingKeyBinderRef(type = MyEntityRoutingKeyBinder.class))
public class MyEntity {

@Id
@GeneratedValue
private Integer id;

public Integer getId() {
return id;
}
}
@@ -0,0 +1,49 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.bridge.routingkeybridge.ormcontext;

import org.hibernate.Session;
import org.hibernate.search.mapper.orm.HibernateOrmExtension;
import org.hibernate.search.mapper.pojo.bridge.RoutingKeyBridge;
import org.hibernate.search.mapper.pojo.bridge.binding.RoutingKeyBindingContext;
import org.hibernate.search.mapper.pojo.bridge.mapping.programmatic.RoutingKeyBinder;
import org.hibernate.search.mapper.pojo.bridge.runtime.RoutingKeyBridgeToRoutingKeyContext;

public class MyEntityRoutingKeyBinder implements RoutingKeyBinder {

@Override
public void bind(RoutingKeyBindingContext context) {
context.dependencies()
.useRootOnly();

context.bridge( new Bridge() );
}

//tag::include[]
private static class Bridge implements RoutingKeyBridge {

@Override
public String toRoutingKey(String tenantIdentifier, Object entityIdentifier,
Object bridgedElement, RoutingKeyBridgeToRoutingKeyContext context) {
Session session = context.extension( HibernateOrmExtension.get() ) // <1>
.session(); // <2>
String routingKey;
// ... do something with the session ...
//end::include[]
/*
* I don't know what to do with the session here,
* so I'm just going to extract data from it.
* This is silly, but at least it allows us to check the session was successfully retrieved.
*/
MyData dataFromSession = (MyData) session.getProperties().get( "test.data.indexed" );
routingKey = dataFromSession.name();
//tag::include[]
return routingKey;
}
}
//end::include[]
}
@@ -0,0 +1,81 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.bridge.routingkeybridge.ormcontext;


import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import javax.persistence.EntityManagerFactory;

import org.hibernate.search.documentation.testsupport.BackendConfigurations;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategyNames;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hibernate.search.util.impl.integrationtest.common.rule.BackendConfiguration;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmSetupHelper;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class RoutingKeyBridgeOrmContextIT {

private static final int SHARD_COUNT = 4;

@Parameterized.Parameters(name = "{0}")
public static Object[] backendSetups() {
return BackendConfigurations.hashBasedSharding( SHARD_COUNT ).toArray();
}

@Rule
public OrmSetupHelper setupHelper;

private EntityManagerFactory entityManagerFactory;

public RoutingKeyBridgeOrmContextIT(BackendConfiguration backendConfiguration) {
this.setupHelper = OrmSetupHelper.withSingleBackend( backendConfiguration );
}

@Before
public void setup() {
entityManagerFactory = setupHelper.start()
.withProperty(
HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY,
AutomaticIndexingSynchronizationStrategyNames.SYNC
)
.setup( MyEntity.class );
}

@Test
public void smoke() {
OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
// See MyDataPropertyBinder
entityManager.setProperty( "test.data.indexed", MyData.INDEXED );

MyEntity myEntity = new MyEntity();
entityManager.persist( myEntity );
} );

OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
SearchSession searchSession = Search.session( entityManager );

List<MyEntity> result = searchSession.search( MyEntity.class )
.where( f -> f.matchAll() )
.routing( "INDEXED" )
.fetchHits( 20 );

assertThat( result ).hasSize( 1 );
} );
}

}
Expand Up @@ -4,7 +4,7 @@
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.routing;
package org.hibernate.search.documentation.mapper.orm.bridge.routingkeybridge.simple;

import javax.persistence.Basic;
import javax.persistence.Entity;
Expand All @@ -16,9 +16,10 @@
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.RoutingKeyBinding;

// tag::include[]
@Entity
@Indexed
@RoutingKeyBinding(binder = @RoutingKeyBinderRef(type = BookRoutingKeyBridge.Binder.class))
@RoutingKeyBinding(binder = @RoutingKeyBinderRef(type = BookGenreRoutingKeyBinder.class)) // <1>
public class Book {

@Id
Expand All @@ -28,12 +29,16 @@ public class Book {
private String title;

@Basic(optional = false)
@KeywordField
@KeywordField // <2>
private Genre genre;

public Book() {
}

// Getters and setters
// ...

// tag::getters-setters[]
public Integer getId() {
return id;
}
Expand All @@ -57,5 +62,6 @@ public Genre getGenre() {
public void setGenre(Genre genre) {
this.genre = genre;
}

// end::getters-setters[]
}
// end::include[]

0 comments on commit 250ff8c

Please sign in to comment.