This chapter will point out the specialties for repository support for MongoDB. This builds on the core repository support explained in Chapter 3, Working with Spring Data Repositories. So make sure you've got a sound understanding of the basic concepts explained there.
To access domain entities stored in a MongoDB you can leverage our sophisticated repository support that eases implementing those quite significantly. To do so, simply create an interface for your repository:
Example 5.1. Sample Person entity
public class Person { @Id private String id; private String firstname; private String lastname; private Address address; // … getters and setters omitted }
We have a quite simple domain object here. Note that it has a
property named id
of typeObjectId
. The
default serialization mechanism used in
MongoTemplate
(which is backing the repository
support) regards properties named id as document id. Currently we
supportString
, ObjectId
and
BigInteger
as id-types.
Example 5.2. Basic repository interface to persist Person entities
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> { // additional custom finder methods go here }
Right now this interface simply serves typing purposes but we will add additional methods to it later. In your Spring configuration simply add
Example 5.3. General MongoDB repository Spring configuration
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd"> <mongo:mongo id="mongo" /> <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate"> <constructor-arg ref="mongo" /> <constructor-arg value="databaseName" /> </bean> <mongo:repositories base-package="com.acme.*.repositories" /> </beans>
This namespace element will cause the base packages to be scanned
for interfaces extending MongoRepository
and create Spring beans for each of them found. By default the
repositories will get a MongoTemplate
Spring bean
wired that is called mongoTemplate
, so you only need to
configure mongo-template-ref
explicitly if you deviate from
this convention.
If you'd rather like to go with JavaConfig use the
@EnableMongoRepositories
annotation. The
annotation carries the very same attributes like the namespace element. If
no base package is configured the infrastructure will scan the package of
the annotated configuration class.
Example 5.4. JavaConfig for repositories
@Configuration @EnableMongoRepositories class ApplicationConfig extends AbstractMongoConfiguration { @Override protected String getDatabaseName() { return "e-store"; } @Override public Mongo mongo() throws Exception { return new Mongo(); } @Override protected String getMappingBasePackage() { return "com.oreilly.springdata.mongodb" } }
As our domain repository extends
PagingAndSortingRepository
it provides you
with CRUD operations as well as methods for paginated and sorted access to
the entities. Working with the repository instance is just a matter of
dependency injecting it into a client. So accessing the second page of
Person
s at a page size of 10 would simply look
something like this:
Example 5.5. Paging access to Person entities
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class PersonRepositoryTests { @Autowired PersonRepository repository; @Test public void readsFirstPageCorrectly() { Page<Person> persons = repository.findAll(new PageRequest(0, 10)); assertThat(persons.isFirstPage(), is(true)); } }
The sample creates an application context with Spring's unit test
support which will perform annotation based dependency injection into test
cases. Inside the test method we simply use the repository to query the
datastore. We hand the repository a PageRequest
instance that requests the first page of persons at a page size of
10.
Most of the data access operations you usually trigger on a repository result a query being executed against the MongoDB databases. Defining such a query is just a matter of declaring a method on the repository interface
Example 5.6. PersonRepository with query methods
public interface PersonRepository extends PagingAndSortingRepository<Person, String> { List<Person> findByLastname(String lastname); Page<Person> findByFirstname(String firstname, Pageable pageable); Person findByShippingAddresses(Address address); }
The first method shows a query for all people with the given
lastname. The query will be derived parsing the method name for
constraints which can be concatenated with And
and
Or
. Thus the method name will result in a query
expression of{"lastname" : lastname}
. The second example
shows how pagination is applied to a query. Just equip your method
signature with a Pageable
parameter and let
the method return a Page
instance and we
will automatically page the query accordingly. The third examples shows
that you can query based on properties which are not a primitive
type.
Note | |
---|---|
Note that for version 1.0 we currently don't support referring to
parameters that are mapped as |
Table 5.1. Supported keywords for query methods
Keyword | Sample | Logical result |
---|---|---|
GreaterThan | findByAgeGreaterThan(int
age) | {"age" : {"$gt" : age}} |
GreaterThanEqual | findByAgeGreaterThanEqual(int
age) | {"age" : {"$gte" : age}} |
LessThan | findByAgeLessThan(int
age) | {"age" : {"$lt" : age}} |
LessThanEqual | findByAgeLessThanEqual(int
age) | {"age" : {"$lte" : age}} |
Between | findByAgeBetween(int from, int
to) | {"age" : {"$gt" : from, "$lt" : to}} |
In | findByAgeIn(Collection ages)
| {"age" : {"$in" : [ages...]}} |
NotIn | findByAgeNotIn(Collection ages)
| {"age" : {"$nin" : [ages...]}} |
IsNotNull ,
NotNull | findByFirstnameNotNull() | {"age" : {"$ne" : null}} |
IsNull ,
Null | findByFirstnameNull() | {"age" : null} |
Like | findByFirstnameLike(String
name) | {"age" : age} ( age as
regex) |
Regex | findByFirstnameRegex(String
firstname) | {"firstname" : {"$regex" : firstname
}} |
(No keyword) | findByFirstname(String
name) | {"age" : name} |
Not | findByFirstnameNot(String
name) | {"age" : {"$ne" : name}} |
Near | findByLocationNear(Point
point) | {"location" : {"$near" : [x,y]}} |
Within | findByLocationWithin(Circle
circle) | {"location" : {"$within" : {"$center" : [ [x, y],
distance]}}} |
Within | findByLocationWithin(Box
box) | {"location" : {"$within" : {"$box" : [ [x1, y1],
x2, y2]}}}True |
IsTrue ,
True | findByActiveIsTrue() | {"active" : true} |
IsFalse ,
False | findByActiveIsFalse() | {"active" : false} |
Exists | findByLocationExists(boolean
exists) | {"location" : {"$exists" : exists }} |
The above keywords can be used in conjunciton with
delete…By
or remove…By
to create queries
deleting matching documents.
Example 5.7. Delete…By
Query
public interface PersonRepository extends MongoRepository<Person, String> { List <Person> deleteByLastname(String lastname); Long deletePersonByLastname(String lastname); }
Using return type List
will
retrieve and return all matching documents before actually deleting
them. A numeric return type directly removes the matching documents
returning the total number of documents removed.
As you've just seen there are a few keywords triggering
geo-spatial operations within a MongoDB query. The Near
keyword allows some further modification. Let's have look at some
examples:
Example 5.8. Advanced Near
queries
public 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 will transparently use
$nearSphere
instead of $code.
Example 5.9. Using 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}}
As you can see using a Distance
equipped
with a Metric
causes
$nearSphere
clause to be added instead of a plain
$near
. Beyond that the actual distance gets calculated
according to the Metrics
used.
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); // {'geoNear' : 'location', 'near' : [x, y] } GeoResults<Person> findByLocationNear(Point location); }
By adding the annotation
org.springframework.data.mongodb.repository.Query
repository finder methods you can specify a MongoDB JSON query string to
use instead of having the query derived from the method name. For
example
public interface PersonRepository extends MongoRepository<Person, String> @Query("{ 'firstname' : ?0 }") List<Person> findByThePersonsFirstname(String firstname); }
The placeholder ?0 lets you substitute the value from the method arguments into the JSON query string.
You can also use the filter property to restrict the set of properties that will be mapped into the Java object. For example,
public interface PersonRepository extends MongoRepository<Person, String> @Query(value="{ 'firstname' : ?0 }", fields="{ 'firstname' : 1, 'lastname' : 1}") List<Person> findByThePersonsFirstname(String firstname); }
This will return only the firstname, lastname and Id properties of the Person objects. The age property, a java.lang.Integer, will not be set and its value will therefore be null.
MongoDB repository support integrates with the QueryDSL project which provides a means to perform type-safe queries in Java. 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 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!)
Adopts better to refactoring changes in domain types
Incremental query definition is easier
Please refer to the QueryDSL documentation which describes how to bootstrap your environment for APT based code generation using Maven or using Ant.
Using QueryDSL you will be able to write queries as shown below
QPerson person = new QPerson("person"); List<Person> result = repository.findAll(person.address.zipCode.eq("C0123")); Page<Person> page = repository.findAll(person.lastname.contains("a"), new PageRequest(0, 2, Direction.ASC, "lastname"));
QPerson
is a class that is generated (via
the Java annotation post processing tool) which is a
Predicate
that allows you to write type safe
queries. Notice that there are no strings in the query other than the
value "C0123".
You can use the generated Predicate
class
via the interface
QueryDslPredicateExecutor
which is shown
below
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, simply inherit from it in addition to other repository interfaces. This is shown below
public interface PersonRepository extends MongoRepository<Person, String>, QueryDslPredicateExecutor<Person> { // additional finder methods go here }
We think you will find this an extremely powerful tool for writing MongoDB queries.
Instances of the repository interfaces are usually created by a
container, which Spring is the most natural choice when working with
Spring Data. As of version 1.3.0 Spring Data MongoDB ships with a custom
CDI extension that allows using the repository abstraction in CDI
environments. The extension is part of the JAR so all you need to do to
activate it is dropping the Spring Data MongoDB JAR into your classpath.
You can now set up the infrastructure by implementing a CDI Producer for
the MongoTemplate
:
class MongoTemplateProducer { @Produces @ApplicationScoped public MongoOperations createMongoTemplate() throws UnknownHostException, MongoException { MongoDbFactory factory = new SimpleMongoDbFactory(new Mongo(), "database"); return new MongoTemplate(factory); } }
The Spring Data MongoDB CDI extension will pick up the
MongoTemplate
available as CDI bean and create a
proxy for a Spring Data repository whenever an bean of a repository type
is requested by the container. Thus obtaining an instance of a Spring
Data repository is a matter of declaring an @Inject
-ed
property:
class RepositoryClient { @Inject PersonRepository repository; public void businessMethod() { List<Person> people = repository.findAll(); } }