Skip to content

Commit 69dc36c

Browse files
DATAES-519 - Add reactive repository support.
Reactive Elasticsearch repository support builds on the core repository support utilizing operations provided via ReactiveElasticsearchOperations executed by a ReactiveElasticsearchClient. Spring Data Elasticsearchs reactive repository support uses Project Reactor as its reactive composition library of choice. There are 3 main interfaces to be used: * ReactiveRepository * ReactiveCrudRepository * ReactiveSortingRepository For Java configuration, use the @EnableReactiveElasticsearchRepositories annotation. The following listing shows how to use Java configuration for a repository: @configuration @EnableReactiveElasticsearchRepositories public class Config extends AbstractReactiveElasticsearchConfiguration { @OverRide public ReactiveElasticsearchClient reactiveElasticsearchClient() { return ReactiveRestClients.create(ClientConfiguration.localhost()); } } Using a repository that extends ReactiveSortingRepository makes all CRUD operations available as well as methods for sorted access to the entities. Working with the repository instance is a matter of dependency injecting it into a client. The repository itself allows defining additional methods backed by the inferred proxy. public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> { Flux<Person> findByFirstname(String firstname); Flux<Person> findByFirstname(Publisher<String> firstname); Flux<Person> findByFirstnameOrderByLastname(String firstname); Flux<Person> findByFirstname(String firstname, Sort sort); Flux<Person> findByFirstname(String firstname, Pageable page); Mono<Person> findByFirstnameAndLastname(String firstname, String lastname); Mono<Person> findFirstByLastname(String lastname); @query("{ \"bool\" : { \"must\" : { \"term\" : { \"lastname\" : \"?0\" } } } }") Flux<Person> findByLastname(String lastname); Mono<Long> countByFirstname(String firstname) Mono<Boolean> existsByFirstname(String firstname) Mono<Long> deleteByFirstname(String firstname) } Original Pull Request: spring-projects#235
1 parent 21a010c commit 69dc36c

File tree

44 files changed

+3133
-33
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3133
-33
lines changed

pom.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@
270270
<includes>
271271
<include>**/*Tests.java</include>
272272
</includes>
273+
<systemPropertyVariables>
274+
<es.set.netty.runtime.available.processors>false</es.set.netty.runtime.available.processors>
275+
</systemPropertyVariables>
273276
</configuration>
274277
</plugin>
275278
</plugins>

src/main/asciidoc/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ include::{spring-data-commons-docs}/repositories.adoc[]
3131
include::reference/elasticsearch-clients.adoc[]
3232
include::reference/data-elasticsearch.adoc[]
3333
include::reference/reactive-elasticsearch-operations.adoc[]
34+
include::reference/reactive-elasticsearch-repositories.adoc[]
3435
include::reference/elasticsearch-misc.adoc[]
3536
:leveloffset: -1
3637

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
[[elasticsearch.reactive.repositories]]
2+
= Reactive Elasticsearch Repositories
3+
4+
Reactive Elasticsearch repository support builds on the core repository support explained in <<repositories>> utilizing
5+
operations provided via <<elasticsearch.reactive.operations>> executed by a <<elasticsearch.clients.reactive>>.
6+
7+
Spring Data Elasticsearchs reactive repository support uses https://projectreactor.io/[Project Reactor] as its reactive
8+
composition library of choice.
9+
10+
There are 3 main interfaces to be used:
11+
12+
* `ReactiveRepository`
13+
* `ReactiveCrudRepository`
14+
* `ReactiveSortingRepository`
15+
16+
[[elasticsearch.reactive.repositories.usage]]
17+
== Usage
18+
19+
To access domain objects stored in a Elasticsearch using a `Repository`, just create an interface for it.
20+
Before you can actually go on and do that you will need an entity.
21+
22+
.Sample `Person` entity
23+
====
24+
[source,java]
25+
----
26+
public class Person {
27+
28+
@Id
29+
private String id;
30+
private String firstname;
31+
private String lastname;
32+
private Address address;
33+
34+
// … getters and setters omitted
35+
}
36+
----
37+
====
38+
39+
NOTE: Please note that the `id` property needs to be of type `String`.
40+
41+
.Basic repository interface to persist Person entities
42+
====
43+
[source]
44+
----
45+
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {
46+
47+
Flux<Person> findByFirstname(String firstname); <1>
48+
49+
Flux<Person> findByFirstname(Publisher<String> firstname); <2>
50+
51+
Flux<Person> findByFirstnameOrderByLastname(String firstname); <3>
52+
53+
Flux<Person> findByFirstname(String firstname, Sort sort); <4>
54+
55+
Flux<Person> findByFirstname(String firstname, Pageable page); <5>
56+
57+
Mono<Person> findByFirstnameAndLastname(String firstname, String lastname); <6>
58+
59+
Mono<Person> findFirstByLastname(String lastname); <7>
60+
61+
@Query("{ \"bool\" : { \"must\" : { \"term\" : { \"lastname\" : \"?0\" } } } }")
62+
Flux<Person> findByLastname(String lastname); <8>
63+
64+
Mono<Long> countByFirstname(String firstname) <9>
65+
66+
Mono<Boolean> existsByFirstname(String firstname) <10>
67+
68+
Mono<Long> deleteByFirstname(String firstname) <11>
69+
}
70+
----
71+
<1> The method shows a query for all people with the given `lastname`.
72+
<2> Finder method awaiting input from `Publisher` to bind parameter value for `firstname`.
73+
<3> Finder method ordering matching documents by `lastname`.
74+
<4> Finder method ordering matching documents by the expression defined via the `Sort` parameter.
75+
<5> Use `Pageable` to pass offset and sorting parameters to the database.
76+
<6> Finder method concating criteria using `And` / `Or` keywords.
77+
<7> Find the first matching entity.
78+
<8> The method shows a query for all people with the given `lastname` looked up by running the annotated `@Query` with given
79+
parameters.
80+
<9> Count all entities with matching `firstname`.
81+
<10> Check if at least one entity with matching `firstname` exists.
82+
<11> Delete all entites with matching `firstname`.
83+
====
84+
85+
[[elasticsearch.reactive.repositories.configuration]]
86+
== Configuration
87+
88+
For Java configuration, use the `@EnableReactiveElasticsearchRepositories` annotation. If no base package is configured,
89+
the infrastructure scans the package of the annotated configuration class.
90+
91+
The following listing shows how to use Java configuration for a repository:
92+
93+
.Java configuration for repositories
94+
====
95+
[source,java]
96+
----
97+
@Configuration
98+
@EnableReactiveElasticsearchRepositories
99+
public class Config extends AbstractReactiveElasticsearchConfiguration {
100+
101+
@Override
102+
public ReactiveElasticsearchClient reactiveElasticsearchClient() {
103+
return ReactiveRestClients.create(ClientConfiguration.localhost());
104+
}
105+
}
106+
----
107+
====
108+
109+
Because the repository from the previous example extends `ReactiveSortingRepository`, all CRUD operations are available
110+
as well as methods for sorted access to the entities. Working with the repository instance is a matter of dependency
111+
injecting it into a client, as the following example shows:
112+
113+
.Sorted access to Person entities
114+
====
115+
[source,java]
116+
----
117+
public class PersonRepositoryTests {
118+
119+
@Autowired ReactivePersonRepository repository;
120+
121+
@Test
122+
public void sortsElementsCorrectly() {
123+
124+
Flux<Person> persons = repository.findAll(Sort.by(new Order(ASC, "lastname")));
125+
126+
// ...
127+
}
128+
}
129+
----
130+
====
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch;
17+
18+
import org.springframework.dao.NonTransientDataAccessResourceException;
19+
20+
/**
21+
* @author Christoph Strobl
22+
* @since 3.2
23+
*/
24+
public class NoSuchIndexException extends NonTransientDataAccessResourceException {
25+
26+
private final String index;
27+
28+
public NoSuchIndexException(String index, Throwable cause) {
29+
super(String.format("Index %s not found.", index), cause);
30+
this.index = index;
31+
}
32+
33+
public String getIndex() {
34+
return index;
35+
}
36+
}

src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ static ClientConfigurationBuilderWithRequiredEndpoint builder() {
4242
return new ClientConfigurationBuilder();
4343
}
4444

45+
/**
46+
* Creates a new {@link ClientConfiguration} instance configured to localhost.
47+
* <p/>
48+
*
49+
* <pre class="code">
50+
* // "localhost:9200"
51+
* ClientConfiguration configuration = ClientConfiguration.localhost();
52+
* </pre>
53+
*
54+
* @return a new {@link ClientConfiguration} instance
55+
* @see ClientConfigurationBuilder#connectedToLocalhost()
56+
*/
57+
static ClientConfiguration localhost() {
58+
return new ClientConfigurationBuilder().connectedToLocalhost().build();
59+
}
60+
4561
/**
4662
* Creates a new {@link ClientConfiguration} instance configured to a single host given {@code hostAndPort}.
4763
* <p/>

src/main/java/org/springframework/data/elasticsearch/client/reactive/DefaultReactiveElasticsearchClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ private static <T> Mono<T> doDecode(ClientResponse response, Class<T> responseTy
545545
return Mono.justOrEmpty(responseType
546546
.cast(ReflectionUtils.invokeMethod(fromXContent, responseType, createParser(mediaType, content))));
547547

548-
} catch (Exception errorParseFailure) {
548+
} catch (Throwable errorParseFailure) { // cause elasticsearch also uses AssertionError
549549

550550
try {
551551
return Mono.error(BytesRestResponse.errorFromXContent(createParser(mediaType, content)));

src/main/java/org/springframework/data/elasticsearch/config/AbstractReactiveElasticsearchConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public abstract class AbstractReactiveElasticsearchConfiguration extends Elastic
4747
* @return never {@literal null}.
4848
*/
4949
@Bean
50-
public ReactiveElasticsearchOperations reactiveElasticsearchOperations() {
50+
public ReactiveElasticsearchOperations reactiveElasticsearchTemplate() {
5151

5252
ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(reactiveElasticsearchClient(),
5353
elasticsearchConverter());

src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchExceptionTranslator.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.springframework.dao.DataAccessException;
2323
import org.springframework.dao.DataAccessResourceFailureException;
2424
import org.springframework.dao.support.PersistenceExceptionTranslator;
25+
import org.springframework.data.elasticsearch.NoSuchIndexException;
26+
import org.springframework.util.CollectionUtils;
2527

2628
/**
2729
* @author Christoph Strobl
@@ -33,15 +35,22 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
3335
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
3436

3537
if (ex instanceof ElasticsearchException) {
36-
// TODO: exception translation
37-
ElasticsearchException elasticsearchExption = (ElasticsearchException) ex;
38-
// elasticsearchExption.get
38+
39+
ElasticsearchException elasticsearchException = (ElasticsearchException) ex;
40+
41+
if (!indexAvailable(elasticsearchException)) {
42+
return new NoSuchIndexException(elasticsearchException.getMetadata("es.index").toString(), ex);
43+
}
3944
}
4045

41-
if(ex.getCause() instanceof ConnectException) {
46+
if (ex.getCause() instanceof ConnectException) {
4247
return new DataAccessResourceFailureException(ex.getMessage(), ex);
4348
}
4449

4550
return null;
4651
}
52+
53+
private boolean indexAvailable(ElasticsearchException ex) {
54+
return !CollectionUtils.contains(ex.getMetadata("es.index_uuid").iterator(), "_na_");
55+
}
4756
}

src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.reactivestreams.Publisher;
2323
import org.springframework.data.domain.Pageable;
2424
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
25+
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
2526
import org.springframework.data.elasticsearch.core.query.Query;
2627
import org.springframework.data.elasticsearch.core.query.StringQuery;
2728
import org.springframework.lang.Nullable;
@@ -451,6 +452,13 @@ default Mono<Long> deleteBy(Query query, Class<?> entityType, @Nullable String i
451452
*/
452453
Mono<Long> deleteBy(Query query, Class<?> entityType, @Nullable String index, @Nullable String type);
453454

455+
/**
456+
* Get the {@link ElasticsearchConverter} used.
457+
*
458+
* @return never {@literal null}
459+
*/
460+
ElasticsearchConverter getElasticsearchConverter();
461+
454462
/**
455463
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on
456464
* {@link ReactiveElasticsearchClient}.

0 commit comments

Comments
 (0)