Skip to content

Commit

Permalink
HSEARCH-3710 Document declaration of dependencies in bridges
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Jul 9, 2020
1 parent 1d04fa1 commit 6c2e635
Show file tree
Hide file tree
Showing 19 changed files with 1,258 additions and 6 deletions.
@@ -1,5 +1,175 @@
[[mapper-orm-bridge-bridgedelement-dependencies]]
= Declaring dependencies to bridged elements

include::todo-placeholder.asciidoc[]
// TODO HSEARCH-3710
== Basics

In order to keep the index synchronized,
Hibernate Search needs to be aware of all the entity properties that are used to produce indexed documents,
so that it can trigger reindexing when they change.

When using a <<mapper-orm-bridge-typebridge,type bridge>> or a <<mapper-orm-bridge-propertybridge,property bridge>>,
the bridge itself decides which entity properties to access during indexing.
Thus, it needs to let Hibernate Search know of its "dependencies" (the entity properties it may access).

This is done through a dedicated DSL, accessible from the `bind(...)` method of <<mapper-orm-bridge-typebridge,`TypeBinder`>>
and <<mapper-orm-bridge-propertybridge,`PropertyBinder`>>.

Below is an example of a type binder that expects to be applied to the `ScientificPaper` type,
and declares a dependency to the paper author's last name and first name.

.Declaring dependencies in a bridge
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/simple/AuthorFullNameBinder.java[tags=binder]
----
<1> Start the declaration of dependencies.
<2> Declare that the bridge will acess the paper's `author` property,
then the author's `firstName` property.
<3> Declare that the bridge will acess the paper's `author` property,
then the author's `lastName` property.
====

The above should be enough to get started, but if you want to know more,
here are a few facts about declaring dependencies.

Paths are relative to the bridged element::
For example:
* for a type bridge on type `ScientificPaper`, path `author` will refer to the value of property `author` on `ScientificPaper` instances.
* for a property bridge on the property `author` of `ScientificPaper`,
path `name` will refer to the value of property `name` on `Author` instances.
Every component of given paths will be considered as a dependency::
You do not need to declare every sub-path.
+
For example, if the path `myProperty.someOtherProperty` is declared as used,
Hibernate Search will automatically assume that `myProperty` is also used.
Only mutable properties need to be declared::
If a property never, ever changes after the entity is first persisted,
then it will never trigger reindexing and Hibernate Search
does not need to know about the dependency.
+
If your bridge only relies on immutable properties,
see <<mapper-orm-bridge-bridgedelement-dependencies-useRootOnly>>.
Associations included in dependency paths need to have an inverse side::
If you declare a dependency that crosses entity boundaries through an association,
and that association has no inverse side in the other entity, an exception will be thrown.
+
For example, when you declare a dependency to path `author.lastName`,
Hibernate Search infers that whenever the last name of an author changes,
its books need to be reindexed.
Thus when it detects an author's last name changed, Hibernate Search will need to retrieve the books to reindex them.
That's why the `author` association in entity `ScientificPaper` needs to have an inverse side in entity `Author`,
e.g. a `books` association.
+
See <<mapper-orm-reindexing>> for more information about these constraints and how to address non-trivial models.

[[mapper-orm-bridge-bridgedelement-dependencies-containers]]
== Traversing non-default containers (map keys, ...)

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

When a path element refers to a property of a container type (`List`, `Map`, `Optional`, ...),
the path will be implicitly resolved to elements of that container.
For example `someMap.otherObject` will resolve to the `otherObject` property
of the _values_ (not the keys) of `someMap`.

If the default resolution is not what you need,
you can explicitly control how to traverse containers by passing `PojoModelPath` objects
instead of just strings:

.Declaring dependencies in a bridge with explicit container extractors
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/containers/simple/Book.java[tags=include;!getters-setters]
----
<1> Apply a custom bridge to the `ScientificPaper` entity.
<2> This (rather complex) map is the one we'll access in the custom bridge.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/containers/simple/BookEditionsForSaleTypeBinder.java[tags=include]
----
<1> Start building a `PojoModelPath`.
<2> Append the `priceByEdition` property (a `Map`) to the path.
<3> Explicitly mention that the bridge will access _keys_ from the `priceByEdition` map -- the paper editions.
Without this, Hibernate Search would have assumed that _values_ are accessed.
<4> Append the `label` property to the path. This is the `label` property in paper editions.
<5> Create the path and pass it to `.use(...)` to declare the dependency.
<6> This is the actual code that accesses the paths as declared above.
====

For property binders applied to a container property,
you can control how to traverse the property itself
by passing a container extractor path as the first argument to `use(...)`:

.Declaring dependencies in a bridge with explicit container extractors for the bridged property
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/containers/property/Book.java[tags=include;!getters-setters]
----
<1> Apply a custom bridge to the `pricesByEdition` property of the `ScientificPaper` entity.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/containers/property/BookEditionsForSalePropertyBinder.java[tags=include]
----
<1> Explicitly mention that the bridge will access _keys_ from the `priceByEdition` property -- the paper editions.
Without this, Hibernate Search would have assumed that _values_ are accessed.
<2> Declare a dependency to the `label` property in paper editions.
<3> This is the actual code that accesses the paths as declared above.
====

[[mapper-orm-bridge-bridgedelement-dependencies-useRootOnly]]
== `useRootOnly()`: declaring no dependency at all

If your bridge only accesses immutable properties,
then it's safe to declare that its only dependency is to the root object.

To do so, call `.dependencies().useRootOnly()`.

[NOTE]
====
Without this call, Hibernate Search will suspect an oversight and will throw an exception on startup.
====

[[mapper-orm-bridge-bridgedelement-dependencies-fromOtherEntity]]
== `fromOtherEntity(...)`: declaring dependencies using the inverse path

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

It is not always possible to represent the dependency as a path
from the bridged element to the values accessed by the bridge.

In particular, when the bridge relies on other components (queries, services) to retrieve another entity,
there may not even be a path from the bridge element to that entity.
In this case, if there is an _inverse_ path from the other entity to the bridged element,
and the bridged element is an entity,
you can simply declare the dependency from the other entity, as shown below.

.Declaring dependencies in a bridge using the inverse path
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/containers/fromotherentity/ScientificPaper.java[tags=include;!getters-setters]
----
<1> Apply a custom bridge to the `ScientificPaper` type.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/bridge/dependencies/containers/fromotherentity/ScientificPapersReferencedByBinder.java[tags=include]
----
<1> Declare that this bridge relies on other entities of type `ScientificPaper`,
and that those other entities reference the indexed entity through their `references` property.
<2> Declare which parts of the other entities are actually used by the bridge.
<3> The bridge retrieves the other entity through a query,
but then uses exclusively the parts that were declared previously.
====

[WARNING]
====
Currently, dependencies declared this way will be ignored when the "other entity" gets deleted.
See https://hibernate.atlassian.net/browse/HSEARCH-3567[HSEARCH-3567] to track progress on solving this problem.
====
@@ -0,0 +1,132 @@
/*
* 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.dependencies.containers.fromotherentity;


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

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 DependenciesFromOtherEntityIT {
@Parameterized.Parameters(name = "{0}")
public static Object[] backendConfigurations() {
return BackendConfigurations.simple().toArray();
}

@Rule
public OrmSetupHelper setupHelper;

private EntityManagerFactory entityManagerFactory;

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

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

@Test
public void smoke() {
OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
ScientificPaper paper1 = new ScientificPaper( 1 );
paper1.setTitle( "Fundamental Ideas of the General Theory of Relativity and the Application of this Theory in Astronomy" );
ScientificPaper paper2 = new ScientificPaper( 2 );
paper2.setTitle( "On the General Theory of Relativity" );
paper2.getReferences().add( paper1 );
ScientificPaper paper3 = new ScientificPaper( 3 );
paper3.setTitle( "Explanation of the Perihelion Motion of Mercury from the General Theory of Relativity" );
paper3.getReferences().add( paper1 );
paper3.getReferences().add( paper2 );

entityManager.persist( paper1 );
entityManager.persist( paper2 );
entityManager.persist( paper3 );
} );

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

assertThat( searchSession.search( ScientificPaper.class )
.where( f -> f.match().field( "referencedBy" )
.matching( "Field Equations Gravitation" ) )
.fetchHits( 20 )
)
.hasSize( 0 );
assertThat( searchSession.search( ScientificPaper.class )
.where( f -> f.match().field( "referencedBy" )
.matching( "Perihelion Motion Mercury" ) )
.fetchHits( 20 )
)
.hasSize( 2 );
assertThat( searchSession.search( ScientificPaper.class )
.where( f -> f.match().field( "referencedBy" )
.matching( "Fundamental Ideas" ) )
.fetchHits( 20 )
)
.hasSize( 0 );
} );
OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
ScientificPaper paper1 = entityManager.find( ScientificPaper.class, 1 );
ScientificPaper paper2 = entityManager.find( ScientificPaper.class, 2 );
ScientificPaper paper3 = entityManager.find( ScientificPaper.class, 3 );
ScientificPaper paper4 = new ScientificPaper( 4 );
paper4.setTitle( "The Field Equations of Gravitation" );
paper4.getReferences().add( paper1 );
paper4.getReferences().add( paper2 );
paper4.getReferences().add( paper3 );

entityManager.persist( paper4 );
} );

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

assertThat( searchSession.search( ScientificPaper.class )
.where( f -> f.match().field( "referencedBy" )
.matching( "Field Equations Gravitation" ) )
.fetchHits( 20 )
)
.hasSize( 3 );
assertThat( searchSession.search( ScientificPaper.class )
.where( f -> f.match().field( "referencedBy" )
.matching( "Perihelion Motion Mercury" ) )
.fetchHits( 20 )
)
.hasSize( 2 );
assertThat( searchSession.search( ScientificPaper.class )
.where( f -> f.match().field( "referencedBy" )
.matching( "Fundamental Ideas" ) )
.fetchHits( 20 )
)
.hasSize( 0 );
} );
}

}
@@ -0,0 +1,61 @@
/*
* 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.dependencies.containers.fromotherentity;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

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

//tag::include[]
@Entity
@Indexed
@TypeBinding(binder = @TypeBinderRef(type = ScientificPapersReferencedByBinder.class)) // <1>
public class ScientificPaper {

@Id
private Integer id;

private String title;

@ManyToMany
private List<ScientificPaper> references = new ArrayList<>();

public ScientificPaper() {
}

// Getters and setters
// ...

//tag::getters-setters[]
public ScientificPaper(Integer id) {
this.id = id;
}

public Integer getId() {
return id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public List<ScientificPaper> getReferences() {
return references;
}
//end::getters-setters[]
}
//end::include[]

0 comments on commit 6c2e635

Please sign in to comment.