Add routing parameter to ElasticsearchOperations.

Original Pull Request #562 
Closes #1218
This commit is contained in:
Peter-Josef Meisch 2021-01-18 23:54:55 +01:00 committed by GitHub
parent aba14c5e11
commit 89d6ae7f49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1409 additions and 304 deletions

View File

@ -33,6 +33,8 @@ include::reference/elasticsearch-auditing.adoc[]
include::{spring-data-commons-docs}/entity-callbacks.adoc[]
include::reference/elasticsearch-entity-callbacks.adoc[leveloffset=+1]
include::reference/elasticsearch-join-types.adoc[]
include::reference/elasticsearch-routing.adoc[]
include::reference/elasticsearch-misc.adoc[]
:leveloffset: -1

View File

@ -0,0 +1,229 @@
[[elasticsearch.jointype]]
= Join-Type implementation
Spring Data Elasticsearch supports the https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html[Join data type] for creating the corresponding index mappings and for storing the relevant information.
== Setting up the data
For an entity to be used in a parent child join relationship, it must have a property of type `JoinField` which must be annotated.
Let's assume a `Statement` entity where a statement may be a _question_, an _answer_, a _comment_ or a _vote_ (a _Builder_ is also shown in this example, it's not necessary, but later used in the sample code):
====
[source,java]
----
@Document(indexName = "statements")
@Routing("routing") <.>
public class Statement {
@Id
private String id;
@Field(type = FieldType.Text)
private String text;
@Field(type = FieldType.Keyword)
private String routing;
@JoinTypeRelations(
relations =
{
@JoinTypeRelation(parent = "question", children = {"answer", "comment"}), <.>
@JoinTypeRelation(parent = "answer", children = "vote") <.>
}
)
private JoinField<String> relation; <.>
private Statement() {
}
public static StatementBuilder builder() {
return new StatementBuilder();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getRouting() {
return routing;
}
public void setRouting(Routing routing) {
this.routing = routing;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public JoinField<String> getRelation() {
return relation;
}
public void setRelation(JoinField<String> relation) {
this.relation = relation;
}
public static final class StatementBuilder {
private String id;
private String text;
private String routing;
private JoinField<String> relation;
private StatementBuilder() {
}
public StatementBuilder withId(String id) {
this.id = id;
return this;
}
public StatementBuilder withRouting(String routing) {
this.routing = routing;
return this;
}
public StatementBuilder withText(String text) {
this.text = text;
return this;
}
public StatementBuilder withRelation(JoinField<String> relation) {
this.relation = relation;
return this;
}
public Statement build() {
Statement statement = new Statement();
statement.setId(id);
statement.setRouting(routing);
statement.setText(text);
statement.setRelation(relation);
return statement;
}
}
}
----
<.> for routing related info see <<elasticsearch.routing>>
<.> a question can have answers and comments
<.> an answer can have votes
<.> the `JoinField` property is used to combine the name (_question_, _answer_, _comment_ or _vote_) of the relation with the parent id.
The generic type must be the same as the `@Id` annotated property.
====
Spring Data Elasticsearch will build the following mapping for this class:
====
[source,json]
----
{
"statements": {
"mappings": {
"properties": {
"_class": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"routing": {
"type": "keyword"
},
"relation": {
"type": "join",
"eager_global_ordinals": true,
"relations": {
"question": [
"answer",
"comment"
],
"answer": "vote"
}
},
"text": {
"type": "text"
}
}
}
}
}
----
====
== Storing data
Given a repository for this class the following code inserts a question, two answers, a comment and a vote:
====
[source,java]
----
void init() {
repository.deleteAll();
Statement savedWeather = repository.save(
Statement.builder()
.withText("How is the weather?")
.withRelation(new JoinField<>("question")) <1>
.build());
Statement sunnyAnswer = repository.save(
Statement.builder()
.withText("sunny")
.withRelation(new JoinField<>("answer", savedWeather.getId())) <2>
.build());
repository.save(
Statement.builder()
.withText("rainy")
.withRelation(new JoinField<>("answer", savedWeather.getId())) <3>
.build());
repository.save(
Statement.builder()
.withText("I don't like the rain")
.withRelation(new JoinField<>("comment", savedWeather.getId())) <4>
.build());
repository.save(
Statement.builder()
.withText("+1 for the sun")
,withRouting(savedWeather.getId())
.withRelation(new JoinField<>("vote", sunnyAnswer.getId())) <5>
.build());
}
----
<1> create a question statement
<2> the first answer to the question
<3> the second answer
<4> a comment to the question
<5> a vote for the first answer, this needs to have the routing set to the weather document, see <<elasticsearch.routing>>.
====
== Retrieving data
Currently native search queries must be used to query the data, so there is no support from standard repository methods. <<repositories.custom-implementations>> can be used instead.
The following code shows as an example how to retrieve all entries that have a _vote_ (which must be _answers_, because only answers can have a vote) using an `ElasticsearchOperations` instance:
====
[source,java]
----
SearchHits<Statement> hasVotes() {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(hasChildQuery("vote", matchAllQuery(), ScoreMode.None))
.build();
return operations.search(query, Statement.class);
}
----
====

View File

@ -1,7 +1,8 @@
[[elasticsearch.misc]]
= Miscellaneous Elasticsearch Operation Support
This chapter covers additional support for Elasticsearch operations that cannot be directly accessed via the repository interface. It is recommended to add those operations as custom implementation as described in <<repositories.custom-implementations>> .
This chapter covers additional support for Elasticsearch operations that cannot be directly accessed via the repository interface.
It is recommended to add those operations as custom implementation as described in <<repositories.custom-implementations>> .
[[elasticsearch.misc.filter]]
== Filter Builder
@ -27,7 +28,8 @@ Page<SampleEntity> sampleEntities = operations.searchForPage(searchQuery, Sample
[[elasticsearch.scroll]]
== Using Scroll For Big Result Set
Elasticsearch has a scroll API for getting big result set in chunks. This is internally used by Spring Data Elasticsearch to provide the implementations of the `<T> SearchHitsIterator<T> SearchOperations.searchForStream(Query query, Class<T> clazz, IndexCoordinates index)` method.
Elasticsearch has a scroll API for getting big result set in chunks.
This is internally used by Spring Data Elasticsearch to provide the implementations of the `<T> SearchHitsIterator<T> SearchOperations.searchForStream(Query query, Class<T> clazz, IndexCoordinates index)` method.
[source,java]
----
@ -76,7 +78,8 @@ while (scroll.hasSearchHits()) {
template.searchScrollClear(scrollId);
----
To use the Scroll API with repository methods, the return type must defined as `Stream` in the Elasticsearch Repository. The implementation of the method will then use the scroll methods from the ElasticsearchTemplate.
To use the Scroll API with repository methods, the return type must defined as `Stream` in the Elasticsearch Repository.
The implementation of the method will then use the scroll methods from the ElasticsearchTemplate.
[source,java]
----
@ -98,209 +101,3 @@ If the class to be retrieved has a `GeoPoint` property named _location_, the fol
----
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
----
[[elasticsearch.misc.jointype]]
== Join-Type implementation
Spring Data Elasticsearch supports the https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html[Join data type] for creating the corresponding index mappings and for storing the relevant information.
=== Setting up the data
For an entity to be used in a parent child join relationship, it must have a property of type `JoinField` which must be annotated.
Let's assume a `Statement` entity where a statement may be a _question_, an _answer_, a _comment_ or a _vote_ (a _Builder_ is also shown in this example, it's not necessary, but later used in the sample code):
====
[source,java]
----
@Document(indexName = "statements")
public class Statement {
@Id
private String id;
@Field(type = FieldType.Text)
private String text;
@JoinTypeRelations(
relations =
{
@JoinTypeRelation(parent = "question", children = {"answer", "comment"}), <1>
@JoinTypeRelation(parent = "answer", children = "vote") <2>
}
)
private JoinField<String> relation; <3>
private Statement() {
}
public static StatementBuilder builder() {
return new StatementBuilder();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public JoinField<String> getRelation() {
return relation;
}
public void setRelation(JoinField<String> relation) {
this.relation = relation;
}
public static final class StatementBuilder {
private String id;
private String text;
private JoinField<String> relation;
private StatementBuilder() {
}
public StatementBuilder withId(String id) {
this.id = id;
return this;
}
public StatementBuilder withText(String text) {
this.text = text;
return this;
}
public StatementBuilder withRelation(JoinField<String> relation) {
this.relation = relation;
return this;
}
public Statement build() {
Statement statement = new Statement();
statement.setId(id);
statement.setText(text);
statement.setRelation(relation);
return statement;
}
}
}
----
<1> a question can have answers and comments
<2> an answer can have votes
<3> the `JoinField` property is used to combine the name (_question_, _answer_, _comment_ or _vote_) of the relation with the parent id. The generic type must be the same as the `@Id` annotated property.
====
Spring Data Elasticsearch will build the following mapping for this class:
====
[source,json]
----
{
"statements": {
"mappings": {
"properties": {
"_class": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"relation": {
"type": "join",
"eager_global_ordinals": true,
"relations": {
"question": [
"answer",
"comment"
],
"answer": "vote"
}
},
"text": {
"type": "text"
}
}
}
}
}
----
====
=== Storing data
Given a repository for this class the following code inserts a question, two answers, a comment and a vote:
====
[source,java]
----
void init() {
repository.deleteAll();
Statement savedWeather = repository.save(
Statement.builder()
.withText("How is the weather?")
.withRelation(new JoinField<>("question")) <1>
.build());
Statement sunnyAnswer = repository.save(
Statement.builder()
.withText("sunny")
.withRelation(new JoinField<>("answer", savedWeather.getId())) <2>
.build());
repository.save(
Statement.builder()
.withText("rainy")
.withRelation(new JoinField<>("answer", savedWeather.getId())) <3>
.build());
repository.save(
Statement.builder()
.withText("I don't like the rain")
.withRelation(new JoinField<>("comment", savedWeather.getId())) <4>
.build());
repository.save(
Statement.builder()
.withText("+1 for the sun")
.withRelation(new JoinField<>("vote", sunnyAnswer.getId())) <5>
.build());
}
----
<1> create a question statement
<2> the first answer to the question
<3> the second answer
<4> a comment to the question
<5> a vote for the first answer
====
=== Retrieving data
Currently native search queries must be used to query the data, so there is no support from standard repository methods. <<repositories.custom-implementations>> can be used instead.
The following code shows as an example how to retrieve all entries that have a _vote_ (which must be _answers_, because only answers can have a vote) using an `ElasticsearchOperations` instance:
====
[source,java]
----
SearchHits<Statement> hasVotes() {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(hasChildQuery("vote", matchAllQuery(), ScoreMode.None))
.build();
return operations.search(query, Statement.class);
}
----
====

View File

@ -5,6 +5,7 @@
== New in Spring Data Elasticsearch 4.2
* Upgrade to Elasticsearch 7.10.0.
* Support for custom routing values
[[new-features.4-1-0]]
== New in Spring Data Elasticsearch 4.1

View File

@ -0,0 +1,106 @@
[[elasticsearch.routing]]
= Routing values
When Elasticsearch stores a document in an index that has multiple shards, it determines the shard to you use based on the _id_ of the document.
Sometimes it is necessary to predefine that multiple documents should be indexed on the same shard (join-types, faster search for related data).
For this Elasticsearch offers the possibility to define a routing, which is the value that should be used to calculate the shard from instead of the _id_.
Spring Data Elasticsearch supports routing definitions on storing and retrieving data in the following ways:
== Routing on join-types
When using join-types (see <<elasticsearch.jointype>>), Spring Data Elasticsearch will automatically use the `parent` property of the entity's `JoinField` property as the value for the routing.
This is correct for all the use-cases where the parent-child relationship has just one level.
If it is deeper, like a child-parent-grandparent relationship - like in the above example from _vote_ -> _answer_ -> _question_ - then the routing needs to explicitly specified by using the techniques described in the next section (the _vote_ needs the _question.id_ as routing value).
== Custom routing values
To define a custom routing for an entity, Spring Data Elasticsearch provides a `@Routing` annotation (reusing the `Statement` class from above):
====
[source,java]
----
@Document(indexName = "statements")
@Routing("routing") <.>
public class Statement {
@Id
private String id;
@Field(type = FieldType.Text)
private String text;
@JoinTypeRelations(
relations =
{
@JoinTypeRelation(parent = "question", children = {"answer", "comment"}),
@JoinTypeRelation(parent = "answer", children = "vote")
}
)
private JoinField<String> relation;
@Nullable
@Field(type = FieldType.Keyword)
private String routing; <.>
// getter/setter...
}
----
<.> This defines _"routing"_ as routing specification
<.> a property with the name _routing_
====
If the `routing` specification of the annotation is a plain string and not a SpEL expression, it is interpreted as the name of a property of the entity, in the example it's the _routing_ property.
The value of this property will then be used as the routing value for all requests that use the entity.
It is also possible to us a SpEL expression in the `@Document` annotation like this:
====
[source,java]
----
@Document(indexName = "statements")
@Routing("@myBean.getRouting(#entity)")
public class Statement{
// all the needed stuff
}
----
====
In this case the user needs to provide a bean with the name _myBean_ that has a method `String getRouting(Object)`. To reference the entity _"#entity"_ must be used in the SpEL expression, and the return value must be `null` or the routing value as a String.
If plain property's names and SpEL expressions are not enough to customize the routing definitions, it is possible to define provide an implementation of the `RoutingResolver` interface. This can then be set on the `ElasticOperations` instance:
====
[source,java]
----
RoutingResolver resolver = ...;
ElasticsearchOperations customOperations= operations.withRouting(resolver);
----
====
The `withRouting()` functions return a copy of the original `ElasticsearchOperations` instance with the customized routing set.
When a routing has been defined on an entity when it is stored in Elasticsearch, the same value must be provided when doing a _get_ or _delete_ operation. For methods that do not use an entity - like `get(ID)` or `delete(ID)` - the `ElasticsearchOperations.withRouting(RoutingResolver)` method can be used like this:
====
[source,java]
----
String id = "someId";
String routing = "theRoutingValue";
// get an entity
Statement s = operations
.withRouting(RoutingResolver.just(routing)) <.>
.get(id, Statement.class);
// delete an entity
operations.withRouting(RoutingResolver.just(routing)).delete(id);
----
<.> `RoutingResolver.just(s)` returns a resolver that will just return the given String.
====

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.Persistent;
/**
* Annotation to enable custom routing values for an entity.
*
* @author Peter-Josef Meisch
* @since 4.2
*/
@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface Routing {
/**
* defines how the routing is determined. Can be either the name of a property or a SpEL expression. See the reference
* documentation for examples how to use this annotation.
*/
String value();
}

View File

@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -61,9 +62,12 @@ import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilde
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.Streamable;
import org.springframework.lang.NonNull;
@ -85,6 +89,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Nullable private EntityOperations entityOperations;
@Nullable private EntityCallbacks entityCallbacks;
@Nullable private RefreshPolicy refreshPolicy;
@Nullable protected RoutingResolver routingResolver;
// region Initialization
protected void initialize(ElasticsearchConverter elasticsearchConverter) {
@ -92,12 +97,32 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
Assert.notNull(elasticsearchConverter, "elasticsearchConverter must not be null.");
this.elasticsearchConverter = elasticsearchConverter;
this.entityOperations = new EntityOperations(this.elasticsearchConverter.getMappingContext());
requestFactory = new RequestFactory(elasticsearchConverter);
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext = this.elasticsearchConverter.getMappingContext();
this.entityOperations = new EntityOperations(mappingContext);
this.routingResolver = new DefaultRoutingResolver((SimpleElasticsearchMappingContext) mappingContext);
requestFactory = new RequestFactory(elasticsearchConverter);
VersionInfo.logVersions(getClusterVersion());
}
/**
* @return copy of this instance.
*/
private AbstractElasticsearchTemplate copy() {
AbstractElasticsearchTemplate copy = doCopy();
if (entityCallbacks != null) {
copy.setEntityCallbacks(entityCallbacks);
}
copy.setRoutingResolver(routingResolver);
return copy;
}
protected abstract AbstractElasticsearchTemplate doCopy();
protected ElasticsearchConverter createElasticsearchConverter() {
MappingElasticsearchConverter mappingElasticsearchConverter = new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
@ -278,6 +303,19 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
return this.delete(getEntityId(entity), index);
}
@Override
public String delete(String id, IndexCoordinates index) {
return doDelete(id, routingResolver.getRouting(), index);
}
@Override
@Deprecated
final public String delete(String id, @Nullable String routing, IndexCoordinates index) {
return doDelete(id, routing, index);
}
protected abstract String doDelete(String id, @Nullable String routing, IndexCoordinates index);
@Override
public List<IndexedObjectInformation> bulkIndex(List<IndexQuery> queries, Class<?> clazz) {
return bulkIndex(queries, getIndexCoordinatesFor(clazz));
@ -621,7 +659,8 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Nullable
private String getEntityId(Object entity) {
Object id = entityOperations.forEntity(entity, elasticsearchConverter.getConversionService()).getId();
Object id = entityOperations.forEntity(entity, elasticsearchConverter.getConversionService(), routingResolver)
.getId();
if (id != null) {
return stringIdRepresentation(id);
@ -632,13 +671,15 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Nullable
public String getEntityRouting(Object entity) {
return entityOperations.forEntity(entity, elasticsearchConverter.getConversionService()).getRouting();
return entityOperations.forEntity(entity, elasticsearchConverter.getConversionService(), routingResolver)
.getRouting();
}
@Nullable
private Long getEntityVersion(Object entity) {
Number version = entityOperations.forEntity(entity, elasticsearchConverter.getConversionService()).getVersion();
Number version = entityOperations.forEntity(entity, elasticsearchConverter.getConversionService(), routingResolver)
.getVersion();
if (version != null && Long.class.isAssignableFrom(version.getClass())) {
return ((Long) version);
@ -651,7 +692,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
private SeqNoPrimaryTerm getEntitySeqNoPrimaryTerm(Object entity) {
EntityOperations.AdaptibleEntity<Object> adaptibleEntity = entityOperations.forEntity(entity,
elasticsearchConverter.getConversionService());
elasticsearchConverter.getConversionService(), routingResolver);
return adaptibleEntity.hasSeqNoPrimaryTerm() ? adaptibleEntity.getSeqNoPrimaryTerm() : null;
}
@ -868,4 +909,24 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
}
}
// endregion
// region routing
private void setRoutingResolver(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
this.routingResolver = routingResolver;
}
@Override
public ElasticsearchOperations withRouting(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
AbstractElasticsearchTemplate copy = copy();
copy.setRoutingResolver(routingResolver);
return copy;
}
// endregion
}

View File

@ -25,6 +25,7 @@ import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.lang.Nullable;
/**
@ -227,9 +228,7 @@ public interface DocumentOperations {
* @param index the index from which to delete
* @return documentId of the document deleted
*/
default String delete(String id, IndexCoordinates index) {
return delete(id, null, index);
}
String delete(String id, IndexCoordinates index);
/**
* Delete the one object with provided id.
@ -239,7 +238,10 @@ public interface DocumentOperations {
* @param index the index from which to delete
* @return documentId of the document deleted
* @since 4.1
* @deprecated since 4.2, use {@link ElasticsearchOperations#withRouting(RoutingResolver)} and
* {@link #delete(String, IndexCoordinates)}
*/
@Deprecated
String delete(String id, @Nullable String routing, IndexCoordinates index);
/**

View File

@ -24,6 +24,7 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverte
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.AliasQuery;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.lang.Nullable;
/**
@ -416,4 +417,16 @@ public interface ElasticsearchOperations extends DocumentOperations, SearchOpera
return Objects.toString(id, null);
}
// endregion
//region routing
/**
* Returns a copy of this instance with the same configuration, but that uses a different {@link RoutingResolver} to
* obtain routing information.
*
* @param routingResolver the {@link RoutingResolver} value, must not be {@literal null}.
* @return DocumentOperations instance
* @since 4.2
*/
ElasticsearchOperations withRouting(RoutingResolver routingResolver);
//endregion
}

View File

@ -93,7 +93,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(ElasticsearchRestTemplate.class);
private final RestHighLevelClient client;
private final ElasticsearchExceptionTranslator exceptionTranslator;
private final ElasticsearchExceptionTranslator exceptionTranslator = new ElasticsearchExceptionTranslator();
// region Initialization
public ElasticsearchRestTemplate(RestHighLevelClient client) {
@ -101,7 +101,6 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(client, "Client must not be null!");
this.client = client;
this.exceptionTranslator = new ElasticsearchExceptionTranslator();
initialize(createElasticsearchConverter());
}
@ -111,11 +110,15 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(client, "Client must not be null!");
this.client = client;
this.exceptionTranslator = new ElasticsearchExceptionTranslator();
initialize(elasticsearchConverter);
}
@Override
protected AbstractElasticsearchTemplate doCopy() {
return new ElasticsearchRestTemplate(client, elasticsearchConverter);
}
// endregion
// region IndexOperations
@ -155,7 +158,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
@Override
@Nullable
public <T> T get(String id, Class<T> clazz, IndexCoordinates index) {
GetRequest request = requestFactory.getRequest(id, index);
GetRequest request = requestFactory.getRequest(id,routingResolver.getRouting(), index);
GetResponse response = execute(client -> client.get(request, RequestOptions.DEFAULT));
DocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
@ -177,7 +180,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
@Override
protected boolean doExists(String id, IndexCoordinates index) {
GetRequest request = requestFactory.getRequest(id, index);
GetRequest request = requestFactory.getRequest(id, routingResolver.getRouting(),index);
request.fetchSourceContext(FetchSourceContext.DO_NOT_FETCH_SOURCE);
return execute(client -> client.get(request, RequestOptions.DEFAULT).isExists());
}
@ -192,7 +195,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
}
@Override
public String delete(String id, @Nullable String routing, IndexCoordinates index) {
protected String doDelete(String id, @Nullable String routing, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");

View File

@ -112,6 +112,14 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
this.client = client;
initialize(elasticsearchConverter);
}
@Override
protected AbstractElasticsearchTemplate doCopy() {
ElasticsearchTemplate elasticsearchTemplate = new ElasticsearchTemplate(client, elasticsearchConverter);
elasticsearchTemplate.setSearchTimeout(searchTimeout);
return elasticsearchTemplate;
}
// endregion
// region IndexOperations
@ -170,7 +178,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
@Override
@Nullable
public <T> T get(String id, Class<T> clazz, IndexCoordinates index) {
GetRequestBuilder getRequestBuilder = requestFactory.getRequestBuilder(client, id, index);
GetRequestBuilder getRequestBuilder = requestFactory.getRequestBuilder(client, id, routingResolver.getRouting(), index);
GetResponse response = getRequestBuilder.execute().actionGet();
DocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
@ -192,7 +201,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
@Override
protected boolean doExists(String id, IndexCoordinates index) {
GetRequestBuilder getRequestBuilder = requestFactory.getRequestBuilder(client, id, index);
GetRequestBuilder getRequestBuilder = requestFactory.getRequestBuilder(client, id, routingResolver.getRouting(), index);
getRequestBuilder.setFetchSource(false);
return getRequestBuilder.execute().actionGet().isExists();
}
@ -207,7 +217,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
}
@Override
public String delete(String id, @Nullable String routing, IndexCoordinates index) {
protected String doDelete(String id, @Nullable String routing, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");

View File

@ -23,6 +23,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
@ -72,14 +73,15 @@ class EntityOperations {
}
/**
* Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService}.
* Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService} and {@link RoutingResolver}.
*
* @param entity must not be {@literal null}.
* @param conversionService must not be {@literal null}.
* @return
* @param routingResolver the {@link RoutingResolver}, must not be {@literal null}
* @return the {@link AdaptibleEntity}
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
<T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService) {
<T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService, RoutingResolver routingResolver) {
Assert.notNull(entity, "Bean must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
@ -88,7 +90,7 @@ class EntityOperations {
return new SimpleMappedEntity((Map<String, Object>) entity);
}
return AdaptibleMappedEntity.of(entity, context, conversionService);
return AdaptibleMappedEntity.of(entity, context, conversionService, routingResolver);
}
/**
@ -304,7 +306,7 @@ class EntityOperations {
/**
* returns the routing for the entity if it is available
*
*
* @return routing if available
* @since 4.1
*/
@ -464,7 +466,7 @@ class EntityOperations {
super(map);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityOperations.UnmappedEntity#getId()
*/
@ -546,32 +548,37 @@ class EntityOperations {
*/
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
private final T bean;
private final ElasticsearchPersistentEntity<?> entity;
private final ConvertingPropertyAccessor<T> propertyAccessor;
private final IdentifierAccessor identifierAccessor;
private final ConversionService conversionService;
private final RoutingResolver routingResolver;
private AdaptibleMappedEntity(ElasticsearchPersistentEntity<?> entity, IdentifierAccessor identifierAccessor,
ConvertingPropertyAccessor<T> propertyAccessor, ConversionService conversionService) {
private AdaptibleMappedEntity(T bean, ElasticsearchPersistentEntity<?> entity,
IdentifierAccessor identifierAccessor, ConvertingPropertyAccessor<T> propertyAccessor,
ConversionService conversionService, RoutingResolver routingResolver) {
super(entity, identifierAccessor, propertyAccessor);
this.bean = bean;
this.entity = entity;
this.propertyAccessor = propertyAccessor;
this.identifierAccessor = identifierAccessor;
this.conversionService = conversionService;
this.routingResolver = routingResolver;
}
static <T> AdaptibleEntity<T> of(T bean,
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context,
ConversionService conversionService) {
ConversionService conversionService, RoutingResolver routingResolver) {
ElasticsearchPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
return new AdaptibleMappedEntity<>(entity, identifierAccessor,
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), conversionService);
return new AdaptibleMappedEntity<>(bean, entity, identifierAccessor,
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), conversionService, routingResolver);
}
@Override
@ -616,7 +623,7 @@ class EntityOperations {
public Number getVersion() {
ElasticsearchPersistentProperty versionProperty = entity.getVersionProperty();
return versionProperty != null ? propertyAccessor.getProperty(versionProperty, Number.class) : null;
return versionProperty != null ? propertyAccessor.getProperty(versionProperty, Number.class) : null;
}
@Override
@ -661,6 +668,12 @@ class EntityOperations {
@Override
public String getRouting() {
String routing = routingResolver.getRouting(bean);
if (routing != null) {
return routing;
}
ElasticsearchPersistentProperty joinFieldProperty = entity.getJoinFieldProperty();
if (joinFieldProperty != null) {
@ -673,6 +686,7 @@ class EntityOperations {
return null;
}
}
}

View File

@ -301,7 +301,7 @@ public interface ReactiveDocumentOperations {
* @param entityType must not be {@literal null}.
* @param index the target index, must not be {@literal null}
* @return a {@link Mono} emitting the {@literal id} of the removed document.
* @deprecated since 4.0, use {@link #delete(String, Class)} or {@link #deleteById(String, IndexCoordinates)}
* @deprecated since 4.0, use {@link #delete(String, Class)} or {@link #delete(String, IndexCoordinates)}
*/
@Deprecated
default Mono<String> delete(String id, Class<?> entityType, IndexCoordinates index) {

View File

@ -20,6 +20,7 @@ import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsea
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.lang.Nullable;
/**
@ -88,6 +89,17 @@ public interface ReactiveElasticsearchOperations extends ReactiveDocumentOperati
*/
ReactiveIndexOperations indexOps(Class<?> clazz);
//region routing
/**
* Returns a copy of this instance with the same configuration, but that uses a different {@link RoutingResolver} to
* obtain routing information.
*
* @param routingResolver the {@link RoutingResolver} value, must not be {@literal null}.
* @return DocumentOperations instance
*/
ReactiveElasticsearchOperations withRouting(RoutingResolver routingResolver);
//endregion
/**
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on
* {@link ReactiveElasticsearchClient}.

View File

@ -75,6 +75,8 @@ import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
@ -104,7 +106,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
private final ReactiveElasticsearchClient client;
private final ElasticsearchConverter converter;
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
private final SimpleElasticsearchMappingContext mappingContext;
private final ElasticsearchExceptionTranslator exceptionTranslator;
private final EntityOperations operations;
protected RequestFactory requestFactory;
@ -114,18 +116,23 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
private @Nullable ReactiveEntityCallbacks entityCallbacks;
private RoutingResolver routingResolver;
// region Initialization
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client) {
this(client, new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()));
}
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, ElasticsearchConverter converter) {
Assert.notNull(client, "client must not be null");
Assert.notNull(converter, "converter must not be null");
this.client = client;
this.converter = converter;
this.mappingContext = converter.getMappingContext();
this.mappingContext = (SimpleElasticsearchMappingContext) converter.getMappingContext();
this.routingResolver = new DefaultRoutingResolver(this.mappingContext);
this.exceptionTranslator = new ElasticsearchExceptionTranslator();
this.operations = new EntityOperations(this.mappingContext);
@ -134,6 +141,16 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
logVersions();
}
private ReactiveElasticsearchTemplate copy() {
ReactiveElasticsearchTemplate copy = new ReactiveElasticsearchTemplate(client, converter);
copy.setRefreshPolicy(refreshPolicy);
copy.setIndicesOptions(indicesOptions);
copy.setEntityCallbacks(entityCallbacks);
copy.setRoutingResolver(routingResolver);
return copy;
}
private void logVersions() {
getClusterVersion() //
.doOnSuccess(VersionInfo::logVersions) //
@ -254,7 +271,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
private <T> T updateIndexedObject(T entity, IndexedObjectInformation indexedObjectInformation) {
AdaptibleEntity<T> adaptibleEntity = operations.forEntity(entity, converter.getConversionService());
AdaptibleEntity<T> adaptibleEntity = operations.forEntity(entity, converter.getConversionService(),
routingResolver);
adaptibleEntity.populateIdIfNecessary(indexedObjectInformation.getId());
ElasticsearchPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
@ -372,7 +390,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
private Mono<Boolean> doExists(String id, IndexCoordinates index) {
return Mono.defer(() -> doExists(requestFactory.getRequest(id, index)));
return Mono.defer(() -> doExists(requestFactory.getRequest(id, routingResolver.getRouting(), index)));
}
/**
@ -395,7 +413,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
private IndexQuery getIndexQuery(Object value) {
AdaptibleEntity<?> entity = operations.forEntity(value, converter.getConversionService());
AdaptibleEntity<?> entity = operations.forEntity(value, converter.getConversionService(), routingResolver);
Object id = entity.getId();
IndexQuery query = new IndexQuery();
@ -448,7 +466,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
private Mono<GetResult> doGet(String id, IndexCoordinates index) {
return Mono.defer(() -> doGet(requestFactory.getRequest(id, index)));
return Mono.defer(() -> doGet(requestFactory.getRequest(id, routingResolver.getRouting(), index)));
}
/**
@ -470,7 +488,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
@Override
public Mono<String> delete(Object entity, IndexCoordinates index) {
AdaptibleEntity<?> elasticsearchEntity = operations.forEntity(entity, converter.getConversionService());
AdaptibleEntity<?> elasticsearchEntity = operations.forEntity(entity, converter.getConversionService(),
routingResolver);
if (elasticsearchEntity.getId() == null) {
return Mono.error(new IllegalArgumentException("entity must have an id"));
@ -503,7 +522,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");
return doDeleteById(id, null, index);
return doDeleteById(id, routingResolver.getRouting(), index);
}
private Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index) {
@ -976,6 +995,25 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
// endregion
// region routing
private void setRoutingResolver(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
this.routingResolver = routingResolver;
}
@Override
public ReactiveElasticsearchOperations withRouting(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
ReactiveElasticsearchTemplate copy = copy();
copy.setRoutingResolver(routingResolver);
return copy;
}
// endregion
protected interface DocumentCallback<T> {
@NonNull

View File

@ -833,12 +833,17 @@ class RequestFactory {
// endregion
// region get
public GetRequest getRequest(String id, IndexCoordinates index) {
return new GetRequest(index.getIndexName(), id);
public GetRequest getRequest(String id, @Nullable String routing, IndexCoordinates index) {
GetRequest getRequest = new GetRequest(index.getIndexName(), id);
getRequest.routing(routing);
return getRequest;
}
public GetRequestBuilder getRequestBuilder(Client client, String id, IndexCoordinates index) {
return client.prepareGet(index.getIndexName(), null, id);
public GetRequestBuilder getRequestBuilder(Client client, String id, @Nullable String routing,
IndexCoordinates index) {
GetRequestBuilder getRequestBuilder = client.prepareGet(index.getIndexName(), null, id);
getRequestBuilder.setRouting(routing);
return getRequestBuilder;
}
public MultiGetRequest multiGetRequest(Query query, Class<?> clazz, IndexCoordinates index) {

View File

@ -44,17 +44,19 @@ public class SearchHit<T> {
private final Map<String, List<String>> highlightFields = new LinkedHashMap<>();
private final Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
@Nullable private final NestedMetaData nestedMetaData;
@Nullable private String routing;
public SearchHit(@Nullable String index, @Nullable String id, float score, @Nullable Object[] sortValues,
@Nullable Map<String, List<String>> highlightFields, T content) {
this(index, id, score, sortValues, highlightFields, null, null, content);
public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score,
@Nullable Object[] sortValues, @Nullable Map<String, List<String>> highlightFields, T content) {
this(index, id, routing, score, sortValues, highlightFields, null, null, content);
}
public SearchHit(@Nullable String index, @Nullable String id, float score, @Nullable Object[] sortValues,
@Nullable Map<String, List<String>> highlightFields, @Nullable Map<String, SearchHits<?>> innerHits,
@Nullable NestedMetaData nestedMetaData, T content) {
public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score,
@Nullable Object[] sortValues, @Nullable Map<String, List<String>> highlightFields,
@Nullable Map<String, SearchHits<?>> innerHits, @Nullable NestedMetaData nestedMetaData, T content) {
this.index = index;
this.id = id;
this.routing = routing;
this.score = score;
this.sortValues = (sortValues != null) ? Arrays.asList(sortValues) : new ArrayList<>();
@ -165,4 +167,13 @@ public class SearchHit<T> {
return "SearchHit{" + "id='" + id + '\'' + ", score=" + score + ", sortValues=" + sortValues + ", content="
+ content + ", highlightFields=" + highlightFields + '}';
}
/**
* @return the routing for this SearchHit, may be {@literal null}.
* @since 4.2
*/
@Nullable
public String getRouting() {
return routing;
}
}

View File

@ -107,6 +107,7 @@ class SearchHitMapping<T> {
return new SearchHit<T>(searchDocument.getIndex(), //
searchDocument.hasId() ? searchDocument.getId() : null, //
searchDocument.getRouting(), //
searchDocument.getScore(), //
searchDocument.getSortValues(), //
getHighlightsAndRemapFieldNames(searchDocument), //
@ -189,6 +190,7 @@ class SearchHitMapping<T> {
Object targetObject = converter.read(targetType, searchDocument);
convertedSearchHits.add(new SearchHit<Object>(searchDocument.getIndex(), //
searchDocument.getId(), //
searchDocument.getRouting(), //
searchDocument.getScore(), //
searchDocument.getSortValues(), //
searchDocument.getHighlightFields(), //

View File

@ -89,4 +89,13 @@ public interface SearchDocument extends Document {
default NestedMetaData getNestedMetaData() {
return null;
}
/**
* @return the routing value for the document
* @since 4.2
*/
@Nullable
default String getRouting() {
return getFieldValue("_routing");
}
}

View File

@ -171,9 +171,18 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
/**
* returns the default settings for an index.
*
*
* @return settings as {@link Document}
* @since 4.1
*/
Document getDefaultSettings();
/**
* Resolves the routing for a bean.
*
* @param bean the bean to resolve the routing for
* @return routing value, may be {@literal null}
*/
@Nullable
String resolveRouting(T bean);
}

View File

@ -24,7 +24,9 @@ import org.elasticsearch.index.VersionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.annotations.Parent;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.join.JoinField;
@ -32,11 +34,11 @@ import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
@ -78,6 +80,8 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private @Nullable VersionType versionType;
private boolean createIndexAndMapping;
private final Map<String, ElasticsearchPersistentProperty> fieldNamePropertyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Expression> routingExpressions = new ConcurrentHashMap<>();
private @Nullable String routing;
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
@ -102,12 +106,21 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.indexStoreType = document.indexStoreType();
this.versionType = document.versionType();
this.createIndexAndMapping = document.createIndex();
Setting setting = AnnotatedElementUtils.getMergedAnnotation(clazz, Setting.class);
if (setting != null) {
this.settingPath = setting.settingPath();
}
}
Setting setting = AnnotatedElementUtils.getMergedAnnotation(clazz, Setting.class);
Routing routingAnnotation = AnnotatedElementUtils.findMergedAnnotation(clazz, Routing.class);
if (setting != null) {
this.settingPath = setting.settingPath();
if (routingAnnotation != null) {
Assert.hasText(routingAnnotation.value(), "@Routing annotation must contain a non-empty value");
this.routing = routingAnnotation.value();
}
}
@ -188,6 +201,8 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return scoreProperty;
}
// endregion
@Override
public void addPersistentProperty(ElasticsearchPersistentProperty property) {
super.addPersistentProperty(property);
@ -351,14 +366,15 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
Expression expression = getExpressionForIndexName(name);
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class) : null;
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class)
: null;
return resolvedName != null ? resolvedName : name;
}
/**
* returns an {@link Expression} for #name if name contains a {@link ParserContext#TEMPLATE_EXPRESSION} otherwise
* returns {@literal null}.
*
*
* @param name the name to get the expression for
* @return Expression may be null
*/
@ -373,7 +389,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
/**
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the name returned by
* {@link #getIndexName()}.
*
*
* @return EvaluationContext
*/
private EvaluationContext getIndexNameEvaluationContext() {
@ -385,6 +401,36 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return getEvaluationContext(null, expressionDependencies);
}
@Override
@Nullable
public String resolveRouting(T bean) {
if (routing == null) {
return null;
}
ElasticsearchPersistentProperty persistentProperty = getPersistentProperty(routing);
if (persistentProperty != null) {
Object propertyValue = getPropertyAccessor(bean).getProperty(persistentProperty);
return propertyValue != null ? propertyValue.toString() : null;
}
try {
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
context.setVariable("entity", bean);
return expression.getValue(context, String.class);
} catch (EvaluationException e) {
throw new InvalidDataAccessApiUsageException(
"Could not resolve expression: " + routing + " for object of class " + bean.getClass().getCanonicalName(), e);
}
}
// endregion
@Override

View File

@ -0,0 +1,57 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.lang.Nullable;
/**
* Default implementation of the {@link RoutingResolver} interface. Returns {@literal null} for the non-bean method and
* delegates to the corresponding persistent entity for the bean-method.
*
* @author Peter-Josef Meisch
* @since 4.2
*/
public class DefaultRoutingResolver implements RoutingResolver {
private final MappingContext<? extends ElasticsearchPersistentEntity, ? extends ElasticsearchPersistentProperty> mappingContext;
public DefaultRoutingResolver(
MappingContext<? extends ElasticsearchPersistentEntity, ? extends ElasticsearchPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
@Override
public String getRouting() {
return null;
}
@Override
@Nullable
public <T> String getRouting(T bean) {
ElasticsearchPersistentEntity<T> persistentEntity = (ElasticsearchPersistentEntity<T>) mappingContext
.getPersistentEntity(bean.getClass());
if (persistentEntity != null) {
return persistentEntity.resolveRouting(bean);
}
return null;
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @since 4.2
*/
public interface RoutingResolver {
/**
* returns the routing when no entity is available.
*
* @return the routing value
*/
@Nullable
String getRouting();
/**
* Returns the routing for a bean.
*
* @param bean the bean to get the routing for
* @return the routing value
*/
@Nullable
<T> String getRouting(T bean);
/**
* Returns a {@link RoutingResolver that always retuns a fixed value}
*
* @param value the value to return
* @return the fixed-value {@link RoutingResolver}
*/
static RoutingResolver just(String value) {
Assert.notNull(value, "value must not be null");
return new RoutingResolver() {
@Override
public String getRouting() {
return value;
}
@Override
public String getRouting(Object bean) {
return value;
}
};
}
}

View File

@ -0,0 +1,6 @@
/**
* classes/interfaces for specification and implementation of Elasticsearch routing.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.elasticsearch.core.routing;

View File

@ -58,7 +58,6 @@ import org.springframework.test.context.ContextConfiguration;
* @author Don Wellington
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class })
@DisplayName("ElasticsearchRestTemplate")
public class ElasticsearchRestTemplateTests extends ElasticsearchTemplateTests {

View File

@ -91,6 +91,7 @@ import org.springframework.data.elasticsearch.core.index.AliasData;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.util.StreamUtils;
import org.springframework.lang.Nullable;
@ -119,6 +120,7 @@ import org.springframework.lang.Nullable;
* @author Roman Puchkovskiy
* @author Subhobrata Dey
*/
@SpringIntegrationTest
public abstract class ElasticsearchTemplateTests {
protected static final String INDEX_NAME_JOIN_SAMPLE_ENTITY = "test-index-sample-join-template";
@ -130,6 +132,7 @@ public abstract class ElasticsearchTemplateTests {
@Autowired protected ElasticsearchOperations operations;
protected IndexOperations indexOperations;
@BeforeEach
public void before() {
indexOperations = operations.indexOps(SampleEntity.class);
@ -3351,7 +3354,8 @@ public abstract class ElasticsearchTemplateTests {
shouldDeleteEntityWithJoinFields(qId2, aId2);
}
void shouldSaveEntityWithJoinFields(String qId1, String qId2, String aId1, String aId2) throws Exception {
// #1218
private void shouldSaveEntityWithJoinFields(String qId1, String qId2, String aId1, String aId2) throws Exception {
SampleJoinEntity sampleQuestionEntity1 = new SampleJoinEntity();
sampleQuestionEntity1.setUuid(qId1);
sampleQuestionEntity1.setText("This is a question");
@ -3391,18 +3395,18 @@ public abstract class ElasticsearchTemplateTests {
new NativeSearchQueryBuilder().withQuery(new ParentIdQueryBuilder("answer", qId1)).build(),
SampleJoinEntity.class);
List<String> hitIds = hits.getSearchHits().stream().map(new Function<SearchHit<SampleJoinEntity>, String>() {
@Override
public String apply(SearchHit<SampleJoinEntity> sampleJoinEntitySearchHit) {
return sampleJoinEntitySearchHit.getId();
}
}).collect(Collectors.toList());
List<String> hitIds = hits.getSearchHits().stream()
.map(sampleJoinEntitySearchHit -> sampleJoinEntitySearchHit.getId()).collect(Collectors.toList());
assertThat(hitIds.size()).isEqualTo(2);
assertThat(hitIds.containsAll(Arrays.asList(aId1, aId2))).isTrue();
hits.forEach(searchHit -> {
assertThat(searchHit.getRouting()).isEqualTo(qId1);
});
}
void shouldUpdateEntityWithJoinFields(String qId1, String qId2, String aId1, String aId2) throws Exception {
private void shouldUpdateEntityWithJoinFields(String qId1, String qId2, String aId1, String aId2) throws Exception {
org.springframework.data.elasticsearch.core.document.Document document = org.springframework.data.elasticsearch.core.document.Document
.create();
document.put("myJoinField", toDocument(new JoinField<>("answer", qId2)));
@ -3444,7 +3448,7 @@ public abstract class ElasticsearchTemplateTests {
assertThat(hitIds.get(0)).isEqualTo(aId1);
}
void shouldDeleteEntityWithJoinFields(String qId2, String aId2) throws Exception {
private void shouldDeleteEntityWithJoinFields(String qId2, String aId2) throws Exception {
Query query = new NativeSearchQueryBuilder().withQuery(new ParentIdQueryBuilder("answer", qId2)).withRoute(qId2)
.build();
operations.delete(query, SampleJoinEntity.class, IndexCoordinates.of(INDEX_NAME_JOIN_SAMPLE_ENTITY));
@ -3822,4 +3826,5 @@ public abstract class ElasticsearchTemplateTests {
@JoinTypeRelation(parent = "question", children = { "answer" }) }) private JoinField<String> myJoinField;
@Field(type = Text) private String text;
}
}

View File

@ -48,14 +48,12 @@ import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
* @author Sascha Woo
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class })
@DisplayName("ElasticsearchTransportTemplate")
public class ElasticsearchTransportTemplateTests extends ElasticsearchTemplateTests {

View File

@ -0,0 +1,129 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.HashSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
*/
class EntityOperationsTest {
@Nullable private static ConversionService conversionService;
@Nullable private static EntityOperations entityOperations;
@Nullable private static SimpleElasticsearchMappingContext mappingContext;
@BeforeAll
static void setUpAll() {
mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(EntityWithRouting.class)));
mappingContext.afterPropertiesSet();
entityOperations = new EntityOperations(mappingContext);
MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext,
new GenericConversionService());
converter.afterPropertiesSet();
conversionService = converter.getConversionService();
}
@Test // #1218
@DisplayName("should return routing from DefaultRoutingAccessor")
void shouldReturnRoutingFromDefaultRoutingAccessor() {
EntityWithRouting entity = EntityWithRouting.builder().id("42").routing("theRoute").build();
EntityOperations.AdaptibleEntity<EntityWithRouting> adaptibleEntity = entityOperations.forEntity(entity,
conversionService, new DefaultRoutingResolver(mappingContext));
String routing = adaptibleEntity.getRouting();
assertThat(routing).isEqualTo("theRoute");
}
@Test // #1218
@DisplayName("should return routing from JoinField when routing value is null")
void shouldReturnRoutingFromJoinFieldWhenRoutingValueIsNull() {
EntityWithRoutingAndJoinField entity = EntityWithRoutingAndJoinField.builder().id("42")
.joinField(new JoinField<>("foo", "foo-routing")).build();
EntityOperations.AdaptibleEntity<EntityWithRoutingAndJoinField> adaptibleEntity = entityOperations.forEntity(entity,
conversionService, new DefaultRoutingResolver(mappingContext));
String routing = adaptibleEntity.getRouting();
assertThat(routing).isEqualTo("foo-routing");
}
@Test // #1218
@DisplayName("should return routing from routing when JoinField is set")
void shouldReturnRoutingFromRoutingWhenJoinFieldIsSet() {
EntityWithRoutingAndJoinField entity = EntityWithRoutingAndJoinField.builder().id("42").routing("theRoute")
.joinField(new JoinField<>("foo", "foo-routing")).build();
EntityOperations.AdaptibleEntity<EntityWithRoutingAndJoinField> adaptibleEntity = entityOperations.forEntity(entity,
conversionService, new DefaultRoutingResolver(mappingContext));
String routing = adaptibleEntity.getRouting();
assertThat(routing).isEqualTo("theRoute");
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "entity-operations-test")
@Routing("routing")
static class EntityWithRouting {
@Id private String id;
private String routing;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "entity-operations-test")
@Routing("routing")
static class EntityWithRoutingAndJoinField {
@Id private String id;
private String routing;
private JoinField<String> joinField;
}
}

View File

@ -59,11 +59,11 @@ class SearchHitSupportTest {
void shouldReturnTheSameListInstanceInSearchHitsAndGetContent() {
List<SearchHit<String>> hits = new ArrayList<>();
hits.add(new SearchHit<>(null, null, 0, null, null, "one"));
hits.add(new SearchHit<>(null, null, 0, null, null, "two"));
hits.add(new SearchHit<>(null, null, 0, null, null, "three"));
hits.add(new SearchHit<>(null, null, 0, null, null, "four"));
hits.add(new SearchHit<>(null, null, 0, null, null, "five"));
hits.add(new SearchHit<>(null, null, null, 0, null, null, "one"));
hits.add(new SearchHit<>(null, null, null, 0, null, null, "two"));
hits.add(new SearchHit<>(null, null, null, 0, null, null, "three"));
hits.add(new SearchHit<>(null, null, null, 0, null, null, "four"));
hits.add(new SearchHit<>(null, null, null, 0, null, null, "five"));
SearchHits<String> originalSearchHits = new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, "scroll",
hits, null);
@ -112,7 +112,7 @@ class SearchHitSupportTest {
@Override
public SearchHit<String> next() {
String nextString = iterator.next();
return new SearchHit<>("index", "id", 1.0f, new Object[0], emptyMap(), nextString);
return new SearchHit<>("index", "id", null, 1.0f, new Object[0], emptyMap(), nextString);
}
}

View File

@ -38,7 +38,7 @@ public class StreamQueriesTest {
// given
List<SearchHit<String>> hits = new ArrayList<>();
hits.add(new SearchHit<String>(null, null, 0, null, null, "one"));
hits.add(getOneSearchHit());
SearchScrollHits<String> searchHits = newSearchScrollHits(hits, "1234");
@ -61,12 +61,16 @@ public class StreamQueriesTest {
}
private SearchHit<String> getOneSearchHit() {
return new SearchHit<String>(null, null, null, 0, null, null, "one");
}
@Test // DATAES-766
public void shouldReturnTotalHits() {
// given
List<SearchHit<String>> hits = new ArrayList<>();
hits.add(new SearchHit<String>(null, null, 0, null, null, "one"));
hits.add(getOneSearchHit());
SearchScrollHits<String> searchHits = newSearchScrollHits(hits, "1234");
@ -85,12 +89,9 @@ public class StreamQueriesTest {
@Test // DATAES-817
void shouldClearAllScrollIds() {
SearchScrollHits<String> searchHits1 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-1");
SearchScrollHits<String> searchHits2 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-2");
SearchScrollHits<String> searchHits3 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-2");
SearchScrollHits<String> searchHits1 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-1");
SearchScrollHits<String> searchHits2 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-2");
SearchScrollHits<String> searchHits3 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-2");
SearchScrollHits<String> searchHits4 = newSearchScrollHits(Collections.emptyList(), "s-3");
Iterator<SearchScrollHits<String>> searchScrollHitsIterator = Arrays
@ -114,12 +115,9 @@ public class StreamQueriesTest {
@Test // DATAES-831
void shouldReturnAllForRequestedSizeOf0() {
SearchScrollHits<String> searchHits1 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-1");
SearchScrollHits<String> searchHits2 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-2");
SearchScrollHits<String> searchHits3 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-2");
SearchScrollHits<String> searchHits1 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-1");
SearchScrollHits<String> searchHits2 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-2");
SearchScrollHits<String> searchHits3 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-2");
SearchScrollHits<String> searchHits4 = newSearchScrollHits(Collections.emptyList(), "s-3");
Iterator<SearchScrollHits<String>> searchScrollHitsIterator = Arrays
@ -139,12 +137,9 @@ public class StreamQueriesTest {
@Test // DATAES-831
void shouldOnlyReturnRequestedCount() {
SearchScrollHits<String> searchHits1 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-1");
SearchScrollHits<String> searchHits2 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-2");
SearchScrollHits<String> searchHits3 = newSearchScrollHits(
Collections.singletonList(new SearchHit<String>(null, null, 0, null, null, "one")), "s-2");
SearchScrollHits<String> searchHits1 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-1");
SearchScrollHits<String> searchHits2 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-2");
SearchScrollHits<String> searchHits3 = newSearchScrollHits(Collections.singletonList(getOneSearchHit()), "s-2");
SearchScrollHits<String> searchHits4 = newSearchScrollHits(Collections.emptyList(), "s-3");
Iterator<SearchScrollHits<String>> searchScrollHitsIterator = Arrays

View File

@ -0,0 +1,139 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.lang.Nullable;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Peter-Josef Meisch
*/
@SpringJUnitConfig({ DefaultRoutingResolverUnitTest.Config.class })
class DefaultRoutingResolverUnitTest {
@Autowired private ApplicationContext applicationContext;
private SimpleElasticsearchMappingContext mappingContext;
@Nullable private RoutingResolver routingResolver;
@Configuration
static class Config {
@Bean
SpelRouting spelRouting() {
return new SpelRouting();
}
}
@BeforeEach
void setUp() {
mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setApplicationContext(applicationContext);
routingResolver = new DefaultRoutingResolver(mappingContext);
}
@Test // #1218
@DisplayName("should throw an exception on unknown property")
void shouldThrowAnExceptionOnUnknownProperty() {
InvalidRoutingEntity entity = new InvalidRoutingEntity("42", "route 66");
assertThatThrownBy(() -> routingResolver.getRouting(entity)).isInstanceOf(InvalidDataAccessApiUsageException.class);
}
@Test // #1218
@DisplayName("should return the routing from the entity")
void shouldReturnTheRoutingFromTheEntity() {
ValidRoutingEntity entity = new ValidRoutingEntity("42", "route 66");
String routing = routingResolver.getRouting(entity);
assertThat(routing).isEqualTo("route 66");
}
@Test // #1218
@DisplayName("should return routing from SpEL expression")
void shouldReturnRoutingFromSpElExpression() {
ValidSpelRoutingEntity entity = new ValidSpelRoutingEntity("42", "route 42");
String routing = routingResolver.getRouting(entity);
assertThat(routing).isEqualTo("route 42");
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "routing-resolver-test")
@Routing("theRouting")
static class ValidRoutingEntity {
@Id private String id;
private String theRouting;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "routing-resolver-test")
@Routing(value = "@spelRouting.getRouting(#entity)")
static class ValidSpelRoutingEntity {
@Id private String id;
private String theRouting;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "routing-resolver-test")
@Routing("unknownProperty")
static class InvalidRoutingEntity {
@Id private String id;
private String theRouting;
}
static class SpelRouting {
@Nullable
public String getRouting(Object o) {
if (o instanceof ValidSpelRoutingEntity) {
return ((ValidSpelRoutingEntity) o).getTheRouting();
}
return null;
}
}
}

View File

@ -0,0 +1,135 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.function.Function;
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@SuppressWarnings("ConstantConditions")
@SpringIntegrationTest
@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class })
public class ElasticsearchOperationsRoutingTests {
private static final String INDEX = "routing-test";
private static final String ID_1 = "id1";
private static final String ID_2 = "id2";
private static final String ID_3 = "id3";
@Autowired ElasticsearchOperations operations;
@Nullable private IndexOperations indexOps;
@BeforeAll
static void beforeAll() {
// check that the used id values go to different shards of the index which is configured to have 5 shards.
// Elasticsearch uses the following function:
Function<String, Integer> calcShard = routing -> Math.floorMod(Murmur3HashFunction.hash(routing), 5);
Integer shard1 = calcShard.apply(ID_1);
Integer shard2 = calcShard.apply(ID_2);
Integer shard3 = calcShard.apply(ID_3);
assertThat(shard1).isNotEqualTo(shard2);
assertThat(shard1).isNotEqualTo(shard3);
assertThat(shard2).isNotEqualTo(shard3);
}
@BeforeEach
void setUp() {
indexOps = operations.indexOps(RoutingEntity.class);
indexOps.delete();
indexOps.create();
indexOps.putMapping();
}
@Test // #1218
@DisplayName("should store data with different routing and be able to get it")
void shouldStoreDataWithDifferentRoutingAndBeAbleToGetIt() {
RoutingEntity entity = RoutingEntity.builder().id(ID_1).routing(ID_2).build();
operations.save(entity);
indexOps.refresh();
RoutingEntity savedEntity = operations.withRouting(RoutingResolver.just(ID_2)).get(entity.id, RoutingEntity.class);
assertThat(savedEntity).isEqualTo(entity);
}
@Test // #1218
@DisplayName("should store data with different routing and be able to delete it")
void shouldStoreDataWithDifferentRoutingAndBeAbleToDeleteIt() {
RoutingEntity entity = RoutingEntity.builder().id(ID_1).routing(ID_2).build();
operations.save(entity);
indexOps.refresh();
String deletedId = operations.withRouting(RoutingResolver.just(ID_2)).delete(entity.id, IndexCoordinates.of(INDEX));
assertThat(deletedId).isEqualTo(entity.getId());
}
@Test // #1218
@DisplayName("should store data with different routing and get the routing in the search result")
void shouldStoreDataWithDifferentRoutingAndGetTheRoutingInTheSearchResult() {
RoutingEntity entity = RoutingEntity.builder().id(ID_1).routing(ID_2).build();
operations.save(entity);
indexOps.refresh();
SearchHits<RoutingEntity> searchHits = operations.search(Query.findAll(), RoutingEntity.class);
assertThat(searchHits.getSearchHits()).hasSize(1);
assertThat(searchHits.getSearchHit(0).getRouting()).isEqualTo(ID_2);
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = INDEX, shards = 5)
@Routing("routing")
static class RoutingEntity {
@Id private String id;
private String routing;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class })
public class ElasticsearchOperationsRoutingTransportTests extends ElasticsearchOperationsRoutingTests {}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.function.Function;
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ReactiveElasticsearchRestTemplateConfiguration.class })
public class ReactiveElasticsearchOperationsRoutingTests {
private static final String INDEX = "routing-test";
private static final String ID_1 = "id1";
private static final String ID_2 = "id2";
private static final String ID_3 = "id3";
@Autowired ReactiveElasticsearchOperations operations;
@Nullable private ReactiveIndexOperations indexOps;
@BeforeAll
static void beforeAll() {
// check that the used id values go to different shards of the index which is configured to have 5 shards.
// Elasticsearch uses the following function:
Function<String, Integer> calcShard = routing -> Math.floorMod(Murmur3HashFunction.hash(routing), 5);
Integer shard1 = calcShard.apply(ID_1);
Integer shard2 = calcShard.apply(ID_2);
Integer shard3 = calcShard.apply(ID_3);
assertThat(shard1).isNotEqualTo(shard2);
assertThat(shard1).isNotEqualTo(shard3);
assertThat(shard2).isNotEqualTo(shard3);
}
@BeforeEach
void setUp() {
indexOps = operations.indexOps(RoutingEntity.class);
indexOps.delete().then(indexOps.create()).then(indexOps.putMapping()).block();
}
@Test // #1218
@DisplayName("should store data with different routing and be able to get it")
void shouldStoreDataWithDifferentRoutingAndBeAbleToGetIt() {
RoutingEntity entity = RoutingEntity.builder().id(ID_1).routing(ID_2).build();
operations.save(entity).then(indexOps.refresh()).block();
RoutingEntity savedEntity = operations.withRouting(RoutingResolver.just(ID_2)).get(entity.id, RoutingEntity.class)
.block();
assertThat(savedEntity).isEqualTo(entity);
}
@Test // #1218
@DisplayName("should store data with different routing and be able to delete it")
void shouldStoreDataWithDifferentRoutingAndBeAbleToDeleteIt() {
RoutingEntity entity = RoutingEntity.builder().id(ID_1).routing(ID_2).build();
operations.save(entity).then(indexOps.refresh()).block();
String deletedId = operations.withRouting(RoutingResolver.just(ID_2)).delete(entity.id, IndexCoordinates.of(INDEX))
.block();
assertThat(deletedId).isEqualTo(entity.getId());
}
@Test // #1218
@DisplayName("should store data with different routing and get the routing in the search result")
void shouldStoreDataWithDifferentRoutingAndGetTheRoutingInTheSearchResult() {
RoutingEntity entity = RoutingEntity.builder().id(ID_1).routing(ID_2).build();
operations.save(entity).then(indexOps.refresh()).block();
List<SearchHit<RoutingEntity>> searchHits = operations.search(Query.findAll(), RoutingEntity.class).collectList()
.block();
assertThat(searchHits).hasSize(1);
assertThat(searchHits.get(0).getRouting()).isEqualTo(ID_2);
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = INDEX, shards = 5)
@Routing("routing")
static class RoutingEntity {
@Id private String id;
private String routing;
}
}

View File

@ -43,6 +43,11 @@ public class ReactiveElasticsearchRestTemplateConfiguration extends AbstractReac
ClientConfiguration.TerminalClientConfigurationBuilder configurationBuilder = ClientConfiguration.builder() //
.connectedTo(elasticsearchHostPort);
String proxy = System.getenv("DATAES_ELASTICSEARCH_PROXY");
if (proxy != null) {
configurationBuilder = configurationBuilder.withProxy(proxy);
}
if (clusterConnectionInfo.isUseSsl()) {
configurationBuilder = ((ClientConfiguration.MaybeSecureClientConfigurationBuilder) configurationBuilder)
.usingSsl();