Query Methods
Most of the data access operations you usually trigger on a repository result in a query being executed against the MongoDB databases. Defining such a query is a matter of declaring a method on the repository interface, as the following example shows:
public interface PersonRepository extends PagingAndSortingRepository<Person, String> {
List<Person> findByLastname(String lastname); (1)
Page<Person> findByFirstname(String firstname, Pageable pageable); (2)
Person findByShippingAddresses(Address address); (3)
Person findFirstByLastname(String lastname) (4)
Stream<Person> findAllBy(); (5)
}
1 | The findByLastname method shows a query for all people with the given last name. The query is derived by parsing the method name for constraints that can be concatenated with And and Or . Thus, the method name results in a query expression of {"lastname" : lastname} . |
2 | Applies pagination to a query. You can equip your method signature with a Pageable parameter and let the method return a Page instance and Spring Data automatically pages the query accordingly. |
3 | Shows that you can query based on properties that are not primitive types. Throws IncorrectResultSizeDataAccessException if more than one match is found. |
4 | Uses the First keyword to restrict the query to only the first result. Unlike <3>, this method does not throw an exception if more than one match is found. |
5 | Uses a Java 8 Stream that reads and converts individual elements while iterating the stream. |
We do not support referring to parameters that are mapped as DBRef in the domain class.
|
The following table shows the keywords that are supported for query methods:
Keyword | Sample | Logical result |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If the property criterion compares a document, the order of the fields and exact equality in the document matters. |
Repository Delete Queries
The keywords in the preceding table can be used in conjunction with delete…By
or remove…By
to create queries that delete matching documents.
Delete…By
Querypublic interface PersonRepository extends MongoRepository<Person, String> {
List <Person> deleteByLastname(String lastname);
Long deletePersonByLastname(String lastname);
}
Using a return type of List
retrieves and returns all matching documents before actually deleting them. A numeric return type directly removes the matching documents, returning the total number of documents removed.
Geo-spatial Repository Queries
As you saw in the preceding table of keywords, a few keywords trigger geo-spatial operations within a MongoDB query. The Near
keyword allows some further modification, as the next few examples show.
The following example shows how to define a near
query that finds all persons with a given distance of a given point:
Near
queriespublic interface PersonRepository extends MongoRepository<Person, String>
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
List<Person> findByLocationNear(Point location, Distance distance);
}
Adding a Distance
parameter to the query method allows restricting results to those within the given distance. If the Distance
was set up containing a Metric
, we transparently use $nearSphere
instead of $code
, as the following example shows:
Distance
with Metrics
Point point = new Point(43.7, 48.8);
Distance distance = new Distance(200, Metrics.KILOMETERS);
… = repository.findByLocationNear(point, distance);
// {'location' : {'$nearSphere' : [43.7, 48.8], '$maxDistance' : 0.03135711885774796}}
Using a Distance
with a Metric
causes a $nearSphere
(instead of a plain $near
) clause to be added. Beyond that, the actual distance gets calculated according to the Metrics
used.
(Note that Metric
does not refer to metric units of measure. It could be miles rather than kilometers. Rather, metric
refers to the concept of a system of measurement, regardless of which system you use.)
Using @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) on the target property forces usage of the $nearSphere operator.
|
Geo-near Queries
Spring Data MongoDb supports geo-near queries, as the following example shows:
public interface PersonRepository extends MongoRepository<Person, String>
// {'geoNear' : 'location', 'near' : [x, y] }
GeoResults<Person> findByLocationNear(Point location);
// No metric: {'geoNear' : 'person', 'near' : [x, y], maxDistance : distance }
// Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance,
// 'distanceMultiplier' : metric.multiplier, 'spherical' : true }
GeoResults<Person> findByLocationNear(Point location, Distance distance);
// Metric: {'geoNear' : 'person', 'near' : [x, y], 'minDistance' : min,
// 'maxDistance' : max, 'distanceMultiplier' : metric.multiplier,
// 'spherical' : true }
GeoResults<Person> findByLocationNear(Point location, Distance min, Distance max);
// {'geoNear' : 'location', 'near' : [x, y] }
GeoResults<Person> findByLocationNear(Point location);
}
MongoDB JSON-based Query Methods and Field Restriction
By adding the org.springframework.data.mongodb.repository.Query
annotation to your repository query methods, you can specify a MongoDB JSON query string to use instead of having the query be derived from the method name, as the following example shows:
public interface PersonRepository extends MongoRepository<Person, String>
@Query("{ 'firstname' : ?0 }")
List<Person> findByThePersonsFirstname(String firstname);
}
The ?0
placeholder lets you substitute the value from the method arguments into the JSON query string.
String parameter values are escaped during the binding process, which means that it is not possible to add MongoDB specific operators through the argument.
|
You can also use the filter property to restrict the set of properties that is mapped into the Java object, as the following example shows:
public interface PersonRepository extends MongoRepository<Person, String>
@Query(value="{ 'firstname' : ?0 }", fields="{ 'firstname' : 1, 'lastname' : 1}")
List<Person> findByThePersonsFirstname(String firstname);
}
The query in the preceding example returns only the firstname
, lastname
and Id
properties of the Person
objects. The age
property, a java.lang.Integer
, is not set and its value is therefore null.
Sorting Query Method results
MongoDB repositories allow various approaches to define sorting order. Let’s take a look at the following example:
public interface PersonRepository extends MongoRepository<Person, String> {
List<Person> findByFirstnameSortByAgeDesc(String firstname); (1)
List<Person> findByFirstname(String firstname, Sort sort); (2)
@Query(sort = "{ age : -1 }")
List<Person> findByFirstname(String firstname); (3)
@Query(sort = "{ age : -1 }")
List<Person> findByLastname(String lastname, Sort sort); (4)
}
1 | Static sorting derived from method name. SortByAgeDesc results in { age : -1 } for the sort parameter. |
2 | Dynamic sorting using a method argument. Sort.by(DESC, "age") creates { age : -1 } for the sort parameter. |
3 | Static sorting via Query annotation. Sort parameter applied as stated in the sort attribute. |
4 | Default sorting via Query annotation combined with dynamic one via a method argument. Sort.unsorted()
results in { age : -1 } . Using Sort.by(ASC, "age") overrides the defaults and creates { age : 1 } . Sort.by
(ASC, "firstname") alters the default and results in { age : -1, firstname : 1 } . |
JSON-based Queries with SpEL Expressions
Query strings and field definitions can be used together with SpEL expressions to create dynamic queries at runtime. SpEL expressions can provide predicate values and can be used to extend predicates with subdocuments.
Expressions expose method arguments through an array that contains all the arguments. The following query uses [0]
to declare the predicate value for lastname
(which is equivalent to the ?0
parameter binding):
public interface PersonRepository extends MongoRepository<Person, String>
@Query("{'lastname': ?#{[0]} }")
List<Person> findByQueryWithExpression(String param0);
}
Expressions can be used to invoke functions, evaluate conditionals, and construct values. SpEL expressions used in conjunction with JSON reveal a side-effect, because Map-like declarations inside of SpEL read like JSON, as the following example shows:
public interface PersonRepository extends MongoRepository<Person, String>
@Query("{'id': ?#{ [0] ? {$exists :true} : [1] }}")
List<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);
}
SpEL in query strings can be a powerful way to enhance queries. However, they can also accept a broad range of unwanted arguments. You should make sure to sanitize strings before passing them to the query to avoid unwanted changes to your query.
Expression support is extensible through the Query SPI: org.springframework.data.repository.query.spi.EvaluationContextExtension
.
The Query SPI can contribute properties and functions and can customize the root object. Extensions are retrieved from the application context
at the time of SpEL evaluation when the query is built. The following example shows how to use EvaluationContextExtension
:
public class SampleEvaluationContextExtension extends EvaluationContextExtensionSupport {
@Override
public String getExtensionId() {
return "security";
}
@Override
public Map<String, Object> getProperties() {
return Collections.singletonMap("principal", SecurityContextHolder.getCurrent().getPrincipal());
}
}
Bootstrapping MongoRepositoryFactory yourself is not application context-aware and requires further configuration
to pick up Query SPI extensions.
|
Type-safe Query Methods
MongoDB repository support integrates with the Querydsl project, which provides a way to perform type-safe queries. To quote from the project description, "Instead of writing queries as inline strings or externalizing them into XML files they are constructed via a fluent API." It provides the following features:
-
Code completion in the IDE (all properties, methods, and operations can be expanded in your favorite Java IDE).
-
Almost no syntactically invalid queries allowed (type-safe on all levels).
-
Domain types and properties can be referenced safely — no strings involved!
-
Adapts better to refactoring changes in domain types.
-
Incremental query definition is easier.
See the QueryDSL documentation for how to bootstrap your environment for APT-based code generation using Maven or Ant.
QueryDSL lets you write queries such as the following:
QPerson person = new QPerson("person");
List<Person> result = repository.findAll(person.address.zipCode.eq("C0123"));
Page<Person> page = repository.findAll(person.lastname.contains("a"),
PageRequest.of(0, 2, Direction.ASC, "lastname"));
QPerson
is a class that is generated by the Java annotation post-processing tool. It is a Predicate
that lets you write type-safe queries. Notice that there are no strings in the query other than the C0123
value.
You can use the generated Predicate
class by using the QuerydslPredicateExecutor
interface, which the following listing shows:
public interface QuerydslPredicateExecutor<T> {
T findOne(Predicate predicate);
List<T> findAll(Predicate predicate);
List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
Long count(Predicate predicate);
}
To use this in your repository implementation, add it to the list of repository interfaces from which your interface inherits, as the following example shows:
public interface PersonRepository extends MongoRepository<Person, String>, QuerydslPredicateExecutor<Person> {
// additional query methods go here
}
Full-text Search Queries
MongoDB’s full-text search feature is store-specific and, therefore, can be found on MongoRepository
rather than on the more general CrudRepository
. We need a document with a full-text index (see “mapping-usage-indexes.text-index” to learn how to create a full-text index).
Additional methods on MongoRepository
take TextCriteria
as an input parameter. In addition to those explicit methods, it is also possible to add a TextCriteria
-derived repository method. The criteria are added as an additional AND
criteria. Once the entity contains a @TextScore
-annotated property, the document’s full-text score can be retrieved. Furthermore, the @TextScore
annotated also makes it possible to sort by the document’s score, as the following example shows:
@Document
class FullTextDocument {
@Id String id;
@TextIndexed String title;
@TextIndexed String content;
@TextScore Float score;
}
interface FullTextRepository extends Repository<FullTextDocument, String> {
// Execute a full-text search and define sorting dynamically
List<FullTextDocument> findAllBy(TextCriteria criteria, Sort sort);
// Paginate over a full-text search result
Page<FullTextDocument> findAllBy(TextCriteria criteria, Pageable pageable);
// Combine a derived query with a full-text search
List<FullTextDocument> findByTitleOrderByScoreDesc(String title, TextCriteria criteria);
}
Sort sort = Sort.by("score");
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("spring", "data");
List<FullTextDocument> result = repository.findAllBy(criteria, sort);
criteria = TextCriteria.forDefaultLanguage().matching("film");
Page<FullTextDocument> page = repository.findAllBy(criteria, PageRequest.of(1, 1, sort));
List<FullTextDocument> result = repository.findByTitleOrderByScoreDesc("mongodb", criteria);
Projections
Spring Data query methods usually return one or multiple instances of the aggregate root managed by the repository. However, it might sometimes be desirable to create projections based on certain attributes of those types. Spring Data allows modeling dedicated return types, to more selectively retrieve partial views of the managed aggregates.
Imagine a repository and aggregate root type such as the following example:
class Person {
@Id UUID id;
String firstname, lastname;
Address address;
static class Address {
String zipCode, city, street;
}
}
interface PersonRepository extends Repository<Person, UUID> {
Collection<Person> findByLastname(String lastname);
}
Now imagine that we want to retrieve the person’s name attributes only. What means does Spring Data offer to achieve this? The rest of this chapter answers that question.
Interface-based Projections
The easiest way to limit the result of the queries to only the name attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example:
interface NamesOnly {
String getFirstname();
String getLastname();
}
The important bit here is that the properties defined here exactly match properties in the aggregate root. Doing so lets a query method be added as follows:
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.
Projections can be used recursively. If you want to include some of the Address
information as well, create a projection interface for that and return that interface from the declaration of getAddress()
, as shown in the following example:
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
On method invocation, the address
property of the target instance is obtained and wrapped into a projecting proxy in turn.
Closed Projections
A projection interface whose accessor methods all match properties of the target aggregate is considered to be a closed projection. The following example (which we used earlier in this chapter, too) is a closed projection:
interface NamesOnly {
String getFirstname();
String getLastname();
}
If you use a closed projection, Spring Data can optimize the query execution, because we know about all the attributes that are needed to back the projection proxy. For more details on that, see the module-specific part of the reference documentation.
Open Projections
Accessor methods in projection interfaces can also be used to compute new values by using the @Value
annotation, as shown in the following example:
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
The aggregate root backing the projection is available in the target
variable.
A projection interface using @Value
is an open projection.
Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.
The expressions used in @Value
should not be too complex — you want to avoid programming in String
variables.
For very simple expressions, one option might be to resort to default methods (introduced in Java 8), as shown in the following example:
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname.concat(" ").concat(getLastname());
}
}
This approach requires you to be able to implement logic purely based on the other accessor methods exposed on the projection interface. A second, more flexible, option is to implement the custom logic in a Spring bean and then invoke that from the SpEL expression, as shown in the following example:
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
Notice how the SpEL expression refers to myBean
and invokes the getFullName(…)
method and forwards the projection target as a method parameter.
Methods backed by SpEL expression evaluation can also use method parameters, which can then be referred to from the expression.
The method parameters are available through an Object
array named args
. The following example shows how to get a method parameter from the args
array:
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
Again, for more complex expressions, you should use a Spring bean and let the expression invoke a method, as described earlier.
Class-based Projections (DTOs)
Another way of defining projections is by using value type DTOs (Data Transfer Objects) that hold properties for the fields that are supposed to be retrieved. These DTO types can be used in exactly the same way projection interfaces are used, except that no proxying happens and no nested projections can be applied.
If the store optimizes the query execution by limiting the fields to be loaded, the fields to be loaded are determined from the parameter names of the constructor that is exposed.
The following example shows a projecting DTO:
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(…) and hashCode() implementations
}
Avoid boilerplate code for projection DTOs
You can dramatically simplify the code for a DTO by using Project Lombok, which provides an
Fields are |
Dynamic Projections
So far, we have used the projection type as the return type or element type of a collection. However, you might want to select the type to be used at invocation time (which makes it dynamic). To apply dynamic projections, use a query method such as the one shown in the following example:
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
This way, the method can be used to obtain the aggregates as is or with a projection applied, as shown in the following example:
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}