Skip to content

Commit

Permalink
HSEARCH-3692 Document property bridges
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere authored and fax4ever committed Sep 23, 2019
1 parent 38bb521 commit a4e96c2
Show file tree
Hide file tree
Showing 18 changed files with 1,034 additions and 11 deletions.
190 changes: 179 additions & 11 deletions documentation/src/main/asciidoc/mapper-orm-bridge.asciidoc
Expand Up @@ -401,36 +401,204 @@ See the javadoc for more information.

=== Basics

include::todo-placeholder.asciidoc[]
// TODO intro
A property bridge, like a <<mapper-orm-bridge-valuebridge,value bridge>>,
is a pluggable component that implements
the mapping of a property to one or more index fields.
It is applied to a property using a custom annotation, specific to each bridge.

Compared to the value bridge, the property bridge is more complex to implement,
but covers a broader range of use cases:

* A property bridge can map a single property to more than one index field.
* A property bridge can support custom parameters, thanks to its custom annotation.
* A property bridge can work correctly when applied to a mutable type,
provided it is implemented correctly.

However, due to its rather flexible nature,
the property bridge does not transparently provide all the features
that come for free with a value bridge.
They can be supported, but have to be implemented manually.
This includes in particular container extractors,
which cannot be combined with a property bridge:
the property bridge must extract container values explicitly.

Implementing a property bridge requires three components:

. A custom annotation, to declare in the entity model that a bridge must be bound to a property.
. A custom implementation of `PropertyBinder`, to actually bind the bridge to a property at bootstrap.
This involves declaring the parts of the property that will be used,
declaring the index fields that will be populated along with their type,
and instantiating the property bridge.
. A custom implementation of `PropertyBridge`, to perform the conversion at runtime.
This involves extracting data from the property, transforming it if necessary,
and pushing it to index fields.

Below is an example of a custom property bridge that maps
a list of invoice line items
to several fields summarizing the invoice.

// Search 5 anchors backward compatibility
[[example-field-bridge]]
// TODO basic example with annotation, binder, bridge, mapping
.Implementing and using a `PropertyBridge`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/simple/InvoiceLineItemsSummaryBinding.java[tags=include]
----
<1> Define an annotation with retention `RUNTIME`.
*Any other retention policy will cause the annotation to be ignored by Hibernate Search*.
<2> Since we're defining a property bridge, allow the annotation
to target either methods (getters) or fields.
<3> Mark this annotation as a property binding,
and instruct Hibernate Search to apply the given binder whenever it finds this annotation.
It is also possible to reference the binder by its name, in the case of a CDI/Spring bean.
<4> Optionally, mark the annotation as documented,
so that it is included in the javadoc of your entities.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/simple/InvoiceLineItemsSummaryBinder.java[tags=binder]
----
<1> The binder must implement the `PropertyBinder` interface,
setting its generic type argument to the type of the corresponding annotation.
<2> Implement the `bind` method in the binder.
<3> Declare the dependencies of the bridge,
i.e. the parts of the property value that the bridge will actually use.
This is *absolutely necessary* in order for Hibernate Search to correctly trigger reindexing
when these parts are modified.
See <<mapper-orm-bridge-bridgedelement-dependencies>>
for more information about declaring dependencies.
<4> Declare the fields that are populated by this bridge.
In this case we're creating a `summary` object field,
which will have multiple sub-fields (see below).
See <<mapper-orm-bridge-index-field-dsl>>
for more information about declaring index fields.
<5> Declare the type of the sub-fields.
We're going to index monetary amounts,
so we will use a `BigDecimal` type with two digits after the decimal point.
See <<mapper-orm-bridge-index-field-type-dsl>>
for more information about declaring index field types.
<6> Call `context.setBridge` to define the property bridge to use,
and pass an instance of the bridge.
<7> Pass a reference to the `summary` object field to the bridge.
<8> Create a sub-field for the `total` amount of the invoice,
a sub-field for the sub-total for `books`,
and a sub-field for the sub-total for `shipping`.
Pass references to these fields to the bridge.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/simple/InvoiceLineItemsSummaryBinder.java[tags=bridge]
----
<1> The bridge must implement the `PropertyBridge` 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> The bridge stores references to the fields:
it will need them when indexing.
<3> Implement the `write` method in the bridge.
This method is called on indexing.
<4> The bridged element is passed as an `Object`,
so cast it to the correct type.
<5> Extract data from the bridged element,
and optionally transform it.
<6> Add an object to the `summary` object field.
Note the `summary` field was declared at the root,
so we call `addObject` directly on the `target` argument.
<7> Add a value to each of the `summary.total`, `summary.books`
and `summary.shipping` fields.
Note the fields were declared as sub-fields of `summary`,
so we call `addValue` on `summaryValue` instead of `target`.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/simple/Invoice.java[tags=include,!getters-setters]
----
<1> Apply the bridge using its custom annotation.
====

=== Passing parameters
// Search 5 anchors backward compatibility
[[_parameterized_bridge]]

include::todo-placeholder.asciidoc[]
By defining attributes in the property binding annotation,
it is possible to pass parameters to the binder:

// Search 5 anchors backward compatibility
[[example-passing-bridge-parameters]]
//TODO parameterized example
.Passing parameters to a `PropertyBinder`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/parameter/InvoiceLineItemsSummaryBinding.java[tags=include]
----
<1> Define an attribute of type String to specify the field name.
Any type supported by annotations can be used.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/parameter/InvoiceLineItemsSummaryBinder.java[tags=include]
----
<1> Implement the `initialize` method in the binder.
<2> Extract the parameter value from the annotation and store it in the binder.
<3> In the `bind` method, use the value of parameters.
Here use the `fieldName` parameter to set the field name,
but we could pass parameters for any purpose:
defining the field as sortable,
defining a normalizer,
...
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/parameter/Invoice.java[tags=include,!getters-setters]
----
<1> Apply the bridge using its custom annotation,
setting the `fieldName` parameter.
====

=== Accessing the ORM session from the bridge

include::todo-placeholder.asciidoc[]
// TODO 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 `PropertyBridge`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/propertybridge/ormcontext/MyDataPropertyBinder.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 say it's supported, give some basic information and link to <<configuration-bean-injection>>
With <<configuration-bean-frameworks,compatible frameworks>>,
Hibernate Search supports injection of beans into the `PropertyBinder`.

The context passed to the property 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.

=== Experimental features

include::todo-placeholder.asciidoc[]
// TODO experimental support for reflection with getBridgedElement (advanced use, no example)
[WARNING]
====
These features are *experimental*.
Usual compatibility policies do not apply: incompatible changes may be introduced in any future release.
====

The context passed to the property binder's `bind` method
exposes a `getBridgedElement` method that gives access to metadata about the property being bound,
in particular its name and type.

The metadata can also be used to inspect the type of the property 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.

[[mapper-orm-bridge-typebridge]]
== Type bridge
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.propertybridge.ormcontext;

public enum MyData {

INDEXED,
PROJECTED;

}
@@ -0,0 +1,56 @@
/*
* 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.propertybridge.ormcontext;

import org.hibernate.Session;
import org.hibernate.search.engine.backend.document.DocumentElement;
import org.hibernate.search.engine.backend.document.IndexFieldReference;
import org.hibernate.search.mapper.orm.HibernateOrmExtension;
import org.hibernate.search.mapper.pojo.bridge.PropertyBridge;
import org.hibernate.search.mapper.pojo.bridge.binding.PropertyBindingContext;
import org.hibernate.search.mapper.pojo.bridge.mapping.programmatic.PropertyBinder;
import org.hibernate.search.mapper.pojo.bridge.runtime.PropertyBridgeWriteContext;

public class MyDataPropertyBinder implements PropertyBinder<MyDataPropertyBinding> {

@Override
public void bind(PropertyBindingContext context) {
context.getDependencies()
.useRootOnly();

context.setBridge( new Bridge(
context.getIndexSchemaElement().field( "myData", f -> f.asString() ).toReference()
) );
}

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

private final IndexFieldReference<String> field;

private Bridge(IndexFieldReference<String> field) {
this.field = field;
}

@Override
public void write(DocumentElement target, Object bridgedElement, PropertyBridgeWriteContext context) {
Session session = context.extension( HibernateOrmExtension.get() ) // <1>
.getSession(); // <2>
// ... 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" );
target.addValue( field, dataFromSession.name() );
//tag::include[]
}
}
//end::include[]
}
@@ -0,0 +1,24 @@
/*
* 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.propertybridge.ormcontext;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.PropertyBinderRef;
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.declaration.PropertyBinding;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD })
@PropertyBinding(binder = @PropertyBinderRef(type = MyDataPropertyBinder.class))
@Documented
public @interface MyDataPropertyBinding {

}
@@ -0,0 +1,37 @@
/*
* 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.propertybridge.ormcontext;

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

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

@Entity
@Indexed
public class MyEntity {

@Id
@GeneratedValue
private Integer id;

@MyDataPropertyBinding
private MyData myData;

public Integer getId() {
return id;
}

public MyData getMyData() {
return myData;
}

public void setMyData(MyData myData) {
this.myData = myData;
}
}

0 comments on commit a4e96c2

Please sign in to comment.