Skip to content

Commit 8d8b07b

Browse files
DATAMONGO-1854 - Add collation when auto creating indices for query method.
We now make sure to include collation information derived from the Query method if the collation is a fixed value.
1 parent 542f1a1 commit 8d8b07b

File tree

5 files changed

+133
-15
lines changed

5 files changed

+133
-15
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ public static Collation of(CollationLocale locale) {
118118
return new Collation(locale);
119119
}
120120

121+
/**
122+
* Parse the given collation string into a {@link Collation}.
123+
*
124+
* @param collation the collation to parse. Can be a simple string like {@code en_US} or a
125+
* {@link Document#parse(String) parsable} document like <code>&#123; 'locale' : '?0' &#125;</code> .
126+
* @return never {@literal null}.
127+
* @throws IllegalArgumentException if {@literal collation} is null.
128+
* @since 2.2
129+
*/
130+
public static Collation parse(String collation) {
131+
132+
Assert.notNull(collation, "Collation must not be null!");
133+
134+
return StringUtils.trimLeadingWhitespace(collation).startsWith("{") ? from(Document.parse(collation))
135+
: of(collation);
136+
}
137+
121138
/**
122139
* Create new {@link Collation} from values in {@link Document}.
123140
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ static Query decorateSort(Query query, Document defaultSort) {
8282
*/
8383
static Query applyCollation(Query query, @Nullable String collationExpression, ConvertingParameterAccessor accessor) {
8484

85-
if(accessor.getCollation() != null) {
85+
if (accessor.getCollation() != null) {
8686
return query.collation(accessor.getCollation());
8787
}
8888

@@ -95,7 +95,7 @@ static Query applyCollation(Query query, @Nullable String collationExpression, C
9595
// TODO: use parameter binding Parser instead of Document.parse once DATAMONGO-2199 is merged.
9696

9797
if (!matcher.find()) {
98-
return query.collation(collationFromString(collationExpression));
98+
return query.collation(Collation.parse(collationExpression));
9999
}
100100

101101
String placeholder = matcher.group();
@@ -104,7 +104,7 @@ static Query applyCollation(Query query, @Nullable String collationExpression, C
104104
if (collationExpression.startsWith("?")) {
105105

106106
if (placeholderValue instanceof String) {
107-
return query.collation(collationFromString(placeholderValue.toString()));
107+
return query.collation(Collation.parse(placeholderValue.toString()));
108108
}
109109
if (placeholderValue instanceof Locale) {
110110
return query.collation(Collation.of((Locale) placeholderValue));
@@ -116,11 +116,7 @@ static Query applyCollation(Query query, @Nullable String collationExpression, C
116116
ObjectUtils.nullSafeClassName(placeholderValue)));
117117
}
118118

119-
return query.collation(collationFromString(collationExpression.replace(placeholder, placeholderValue.toString())));
120-
}
121-
122-
private static Collation collationFromString(String source) {
123-
return source.startsWith("{") ? Collation.from(Document.parse(source)) : Collation.of(source);
119+
return query.collation(Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString())));
124120
}
125121

126122
private static int computeParameterIndex(String parameter) {

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.mongodb.core.MongoOperations;
2828
import org.springframework.data.mongodb.core.index.Index;
2929
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
30+
import org.springframework.data.mongodb.core.query.Collation;
3031
import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
3132
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
3233
import org.springframework.data.repository.core.support.QueryCreationListener;
@@ -93,6 +94,14 @@ public void onCreation(PartTreeMongoQuery query) {
9394
}
9495
}
9596

97+
if (query.getQueryMethod().hasAnnotatedCollation()) {
98+
99+
String collation = query.getQueryMethod().getAnnotatedCollation();
100+
if (!collation.contains("?")) {
101+
index = index.collation(Collation.parse(collation));
102+
}
103+
}
104+
96105
MongoEntityMetadata<?> metadata = query.getQueryMethod().getEntityInformation();
97106
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
98107
LOG.debug(String.format("Created %s!", index));

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,28 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.support;
1717

18-
import static org.mockito.ArgumentMatchers.*;
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.ArgumentMatchers.any;
1920
import static org.mockito.Mockito.*;
21+
import static org.mockito.Mockito.anyString;
2022

23+
import org.bson.Document;
2124
import org.junit.Before;
2225
import org.junit.Test;
2326
import org.junit.runner.RunWith;
2427
import org.mockito.Answers;
28+
import org.mockito.ArgumentCaptor;
2529
import org.mockito.Mock;
2630
import org.mockito.junit.MockitoJUnitRunner;
31+
import org.springframework.data.domain.Sort;
32+
import org.springframework.data.mongodb.core.index.IndexDefinition;
33+
import org.springframework.data.mongodb.core.index.IndexOperations;
2734
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
35+
import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
36+
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
2837
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
2938
import org.springframework.data.repository.query.parser.PartTree;
39+
import org.springframework.data.util.Streamable;
3040

3141
/**
3242
* Unit tests for {@link IndexEnsuringQueryCreationListener}.
@@ -39,26 +49,109 @@ public class IndexEnsuringQueryCreationListenerUnitTests {
3949
IndexEnsuringQueryCreationListener listener;
4050

4151
@Mock IndexOperationsProvider provider;
52+
@Mock PartTree partTree;
53+
@Mock PartTreeMongoQuery partTreeQuery;
54+
@Mock MongoQueryMethod queryMethod;
55+
@Mock IndexOperations indexOperations;
56+
@Mock MongoEntityMetadata entityInformation;
4257

4358
@Before
4459
public void setUp() {
60+
4561
this.listener = new IndexEnsuringQueryCreationListener(provider);
62+
63+
partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
64+
when(partTreeQuery.getTree()).thenReturn(partTree);
65+
when(provider.indexOps(anyString())).thenReturn(indexOperations);
66+
when(queryMethod.getEntityInformation()).thenReturn(entityInformation);
67+
when(entityInformation.getCollectionName()).thenReturn("persons");
4668
}
4769

4870
@Test // DATAMONGO-1753
4971
public void skipsQueryCreationForMethodWithoutPredicate() {
5072

51-
PartTree tree = mock(PartTree.class);
52-
when(tree.hasPredicate()).thenReturn(false);
73+
when(partTree.hasPredicate()).thenReturn(false);
5374

54-
PartTreeMongoQuery query = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
55-
when(query.getTree()).thenReturn(tree);
56-
57-
listener.onCreation(query);
75+
listener.onCreation(partTreeQuery);
5876

5977
verify(provider, times(0)).indexOps(any());
6078
}
6179

80+
@Test // DATAMONGO-1854
81+
public void usesCollationWhenPresentAndFixedValue() {
82+
83+
when(partTree.hasPredicate()).thenReturn(true);
84+
when(partTree.getParts()).thenReturn(Streamable.empty());
85+
when(partTree.getSort()).thenReturn(Sort.unsorted());
86+
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
87+
when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
88+
when(queryMethod.getAnnotatedCollation()).thenReturn("en_US");
89+
90+
listener.onCreation(partTreeQuery);
91+
92+
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
93+
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
94+
95+
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
96+
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US")));
97+
}
98+
99+
@Test // DATAMONGO-1854
100+
public void usesCollationWhenPresentAndFixedDocumentValue() {
101+
102+
when(partTree.hasPredicate()).thenReturn(true);
103+
when(partTree.getParts()).thenReturn(Streamable.empty());
104+
when(partTree.getSort()).thenReturn(Sort.unsorted());
105+
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
106+
when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
107+
when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : 'en_US' }");
108+
109+
listener.onCreation(partTreeQuery);
110+
111+
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
112+
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
113+
114+
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
115+
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US")));
116+
}
117+
118+
@Test // DATAMONGO-1854
119+
public void skipsCollationWhenPresentButDynamic() {
120+
121+
when(partTree.hasPredicate()).thenReturn(true);
122+
when(partTree.getParts()).thenReturn(Streamable.empty());
123+
when(partTree.getSort()).thenReturn(Sort.unsorted());
124+
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
125+
when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
126+
when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : '?0' }");
127+
128+
listener.onCreation(partTreeQuery);
129+
130+
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
131+
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
132+
133+
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
134+
assertThat(indexDefinition.getIndexOptions()).isEmpty();
135+
}
136+
137+
@Test // DATAMONGO-1854
138+
public void skipsCollationWhenNotPresent() {
139+
140+
when(partTree.hasPredicate()).thenReturn(true);
141+
when(partTree.getParts()).thenReturn(Streamable.empty());
142+
when(partTree.getSort()).thenReturn(Sort.unsorted());
143+
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
144+
when(queryMethod.hasAnnotatedCollation()).thenReturn(false);
145+
146+
listener.onCreation(partTreeQuery);
147+
148+
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
149+
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
150+
151+
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
152+
assertThat(indexDefinition.getIndexOptions()).isEmpty();
153+
}
154+
62155
interface SampleRepository {
63156

64157
Object findAllBy();

src/main/asciidoc/reference/mongodb.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,9 @@ and `Document` (eg. new Document("locale", "en_US"))
15981598
<4> Dynamic collation depending on 2nd method argument.
15991599
<5> Apply the `Collation` method parameter to the query.
16001600
<6> The `Collation` method parameter overrides the default `collation` from `@Query` if not null.
1601+
1602+
NOTE: In case you enabled the automatic index creation for repository finder methods a potential static collation definition,
1603+
as shown in (1) and (2), will be included when creating the index.
16011604
====
16021605

16031606
[[mongo.jsonSchema]]

0 commit comments

Comments
 (0)