There are already a lot of tools to access MongoDB collections using Java. Each driver or wrapper has it’s own distinct features and advantages. The focus of Immutables repository generation is to provide the best possible API that matches well for storing documents expressed as immutable objects.
Future
One of the side goals of this module was to demonstrate that Java DSLs and APIs could be actually a lot less ugly than they usually are.
Generated repositories wrap the infrastructure of the official Java driver and BSON serialization.
// Define repository for collection "items".
@Value.Immutable
@Mongo.Repository("items")
public abstract class Item {
@Mongo.Id
public abstract long id();
public abstract String name();
public abstract Set<Integer> values();
public abstract Optional<String> comment();
}
// Instantiate generated repository
ItemRepository items = new ItemRepository(
RepositorySetup.forUri("mongodb://localhost/test"));
// Create item
Item item = ImmutableItem.builder()
.id(1)
.name("one")
.addValues(1, 2)
.build();
// Insert async
items.insert(item); // returns future
Optional<Item> modifiedItem = items.findById(1)
.andModifyFirst() // findAndModify
.addValues(1) // $addToSet
.setComment("present") // $set
.returningNew()
.update() // returns future
.getUnchecked();
// Update all matching documents
items.update(
ItemRepository.criteria()
.idIn(1, 2, 3)
.nameNot("Nameless")
.valuesNonEmpty())
.clearComment()
.updateAll();
In addition to code annotation-processor, it’s necessary to add the mongo
annotation module and runtime library, including some required transitive dependencies.
Mongo artifact required to be used for compilation as well be available at runtime. By default, Mongo adapter works with Gson module, however there are ways to register external CodecRegistry to allow custom serialization and deserialization using other libraries (eg. Jackson).
Snippet of Maven dependencies:
<dependency>
<groupId>org.immutables</groupId>
<artifactId>value</artifactId>
<version>2.10.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.immutables</groupId>
<artifactId>mongo</artifactId>
<version>2.10.1</version>
</dependency>
In order to enable repository generation, put an org.immutables.mongo.Mongo.Repository
annotation on a abstract value class alongside an org.immutables.value.Value.Immutable
annotation.
A repository which accesses a collection of documents will be generated
as a class with a Repository
suffix in the same package.
By default, the mapped collection name is derived from abstract value class name: For a
UserDocument
class, the collection name will be userDocument
. However, the name is customizable
using a value
annotation attribute:
import org.immutables.mongo.Mongo;
import org.immutables.value.Value;
@Value.Immutable
@Gson.TypeAdapters // you can put TypeAdapters on a package instead
@Mongo.Repository("user")
public abstract class UserDocument {
...
}
Once the repository class is generated, it’s possible to instantiate this class using the new
operator. You need to supply a org.immutables.common.repository.RepositorySetup
as a
constructor argument. Setup can be shared by all repositories for a single MongoDB
database. RepositorySetup
combines the definition of a thread pool, MongoDB database,
and a configured com.google.gson.Gson
instance.
Luckily, to get started (and for simpler applications), there’s an easy way to create a setup
using RepositorySetup.forUri
factory method. Pass a MongoDB connection string and a setup
will be created with default settings.
RepositorySetup setup = RepositorySetup.forUri("mongodb://localhost/test");
To get a test database running on the default port on a local machine, just launch mongod
.
To fully customize setting use RepositorySetup
builder:
MongoClient mongoClient = ...
ListeningExecutorService executor = ...
GsonBuilder gsonBuilder = new GsonBuilder();
...
RepositorySetup setup = RepositorySetup.builder()
.database(mongoClient.getDB("test"))
.executor(executor)
.gson(gsonBuilder.create())
.build();
See getting started with java driver for an explanation how to create a MongoClient
.
It is highly recommended to have explicit _id
field for MongoDB documents. Use the @Mongo.Id
annotation to declare an id
attribute. The @Mongo.Id
annotation acts as an alias to
@Gson.Named("_id")
, which can also be used.
@Value.Immutable
@Gson.TypeAdapters
@Mongo.Repository("user")
public abstract class UserDocument {
@Mongo.Id
public abstract int id();
...
}
An identifier attribute can be of any type that is marshaled to a valid BSON type that can
be used as _id
field in MongoDB. The Java attribute name is irrelevant as long as it will be
generated marshaled as _id
(annotated with @Gson.Named("_id")
or @Mongo.Id
).
In some cases you may need to use special type ObjectID
for _id
or other fields. In order to
do this, Immutables provides the wrapper type org.immutables.mongo.types.Id
. Use the static
factory methods of org.immutables.mongo.types.Id
class to construct instances that correspond
to MongoDB’ ObjectID
. Here’s example of an auto-generated identifier:
import org.immutables.value.Value;
import org.immutables.gson.Gson;
import org.immutables.mongo.Mongo;
import org.immutables.mongo.types.Id;
@Value.Immutable
@Gson.TypeAdapters
@Mongo.Repository("events")
public abstract class EventRecord {
@Mongo.Id
@Value.Default
public Id id() {
return Id.generate();
}
...
}
BSON/JSON documents —-
Reading and writing mongo documents requries conversion into BSON format. Immutables provides BSON adapters for common JSON libraries like Jackson (experimental) and GSON so users can reuse their mapping API while persisting objects in binary (BSON) format.
For historical reasons, GSON is used by default for mongo repositories. However, it is possible to register any CodecRegistry (even PojoCodec). Internally, Jackson and Gson have their own CodecRegistry implementations.
All values used to model documents should have GSON type adapters registered. Use
@Gson.TypeAdapters
on types or packages to generate type adapters for enclosed value types. When
using RepositorySetup.forUri
, all type adapters will be auto-registered from the classpath. When
using custom RepositorySetup
, register type adapters on a Gson
instance using GsonBuilder
as shown in GSON guide.
A large portion of the things you need to know to create MongoDB mapped documents with GSON is described in GSON guide
Since release 2.7.2 immutables has added experimental support for jackson. You can use provided helper classes to bridge between ObjectMapper and CodecRegistry as shown below:
ObjectMapper mapper = new ObjectMapper()
// support for mongo driver (bson) specific types like Document, DBObject etc.
.registerModule(JacksonCodecs.module(MongoClient.getDefaultCodecRegistry()))
.registerModule(new GuavaModule());
RepositorySetup setup = RepositorySetup.builder()
.database(database)
.codecRegistry(JacksonCodecs.registryFromMapper(mapper))
// ...
.build();
Jackson adapter for CodecRegistry will delegate all calls to native BsonReader and BsonWriter without intermediate object reprsentation (eg. String or byte[]) thus avoiding extra parsing and memory allocation.
Please note that jackson support is marked as @Beta so API is subject to change or even removal.
Operations ———-
@Value.Immutable
@Gson.TypeAdapters
@Mongo.Repository("posts")
public abstract class PostDocument {
@Mongo.Id
public abstract long id();
public abstract String content();
public abstract List<Integer> ratings();
@Value.Default
public int version() {
return 0;
}
}
// Instantiate generated repository
PostDocumentRepository posts = new PostDocumentRepository(
RepositorySetup.forUri("mongodb://localhost/test"));
Insert single or iterable of documents using insert
methods.
posts.insert(
ImmutablePostDocument.builder()
.id(1)
.content("a")
.build());
posts.insert(
ImmutableList.of(
ImmutablePostDocument.builder()
.id(2)
.content("b")
.build(),
ImmutablePostDocument.builder()
.id(3)
.content("c")
.build(),
ImmutablePostDocument.builder()
.id(4)
.content("d")
.build()));
Update or insert full document content by _id
using the upsert
method:
posts.upsert(
ImmutablePostDocument.builder()
.id(1)
.content("a1")
.build());
posts.upsert(
ImmutablePostDocument.builder()
.id(10)
.content("!!!")
.addRatings(2)
.build());
If document with _id
10 is not found, then it will be created, otherwise updated.
To find a document, you need to provide criteria object. Search criteria objects are generated to reflect fields of
the document. Empty criteria objects are obtained by using the criteria()
static factory method on a generated repository.
Criteria objects are immutable and can be stored as constants or otherwise safely passed around.
Criteria objects have methods corresponding to document attributes and relevant constraints.
Criteria where = posts.criteria();
List<PostDocument> documents =
posts.find(where.contentStartsWith("a"))
.fetchAll()
.getUnchecked();
Optional<PostDocument> document =
posts.find(
where.content("!!!")
.ratingsNonEmpty())
.fetchFirst()
.getUnchecked();
List<PostDocument> limited =
posts.find(
where.contentStartsWith("a")
.or()
.contentStartsWith("b"))
.orderById()
.skip(5)
.fetchWithLimit(10)
.getUnchecked();
With each constraint, a new immutable criteria is returned which composes constraints with the and logic. Constraints
can be composed with or logic by explicitly delimiting with .or()
method,
effectively forming DNF consisting of constraints.
The find
method returns an uncompleted operation, which is subject to configuration via Finder
object methods,
discover these configuration methods, use them as needed, then invoke finalizing operation which returns future
of result.
For convenience, there are methods to lookup by _id
and to find all documents. These methods do not need criteria objects.
posts.findById(10).fetchFirst();
// Fetch all? Ok
posts.findAll().fetchAll();
Note that findById
method might be named differently if your document has its attribute with a name other than id
in Java.
MongoDB has a feature to return a subset of fields in results. In order to preserve the consistency of immutable
document objects created during unmarshaling, a repository only allows the exclusion of optional fields such
as collection attributes and optional attributes.
Use exclude*
methods on Finder
objects to configure attribute exclusion.
boolean isTrue =
posts.findById(10)
.excludeRatings()
.fetchFirst()
.getUnchecked()
.ratings()
.isEmpty();
Use Finder
to specify ordering by attributes and direction. Ordering is used for fetching results as well
as finding the first matching object to modify.
posts.find(where.contentNot("b"))
.orderByContent()
.orderByIdDesceding()
.deleteFirst();
posts.findAll()
.orderByContent()
.fetchWithLimit(10);
Looking for delete operations? Well, we found good place for them, but probably not a very obvious one!
Delete operations are defined on the same Finder
object:
posts.findById(1).deleteFirst();
int deletedDocumentsCount = posts.find(where.content(""))
.deleteAll()
.getUnchecked();
// Delete all? Ok
posts.findAll().deleteAll();
Update, find, and modify operations support incremental updates of the documents matching a criteria. Incremental update operations are used to update particular fields. Some fields may need to be initialized if a document is to be created via upsert operation.
Optional<PostDocument> updatedDocument =
posts.findById(2)
.andModifyFirst()
.addRatings(5)
.setContent("bbb")
.returningNew()
.update()
.getUnchecked();
posts.update(where.ratingsEmpty())
.addRatings(3)
.updateAll();
posts.findById(111)
.andModifyFirst()
.incrementVersion(1)
.initContent("2")
.addRatings(5)
.upsert();
For usecases when only read operations are required one can customize repository generation with readonly
annotation parameter.
When set to true
(it is false
by default) write, delete and update methods will not be available:
@Value.Immutable
@Mongo.Repository(readonly = true) // don't generate any write / delete / update methods
public abstract class Item {
// ...
}
To omit indexing operations use index = false
parameter (indexing is enabled by default).
If you want to ensure indices using code rather than the administrative tools,
you can use an Indexer
object, which ensures indexing with particular fields.
See the methods of Indexer
object.
// Compound index on content and ratings
posts.index()
.withContent()
.withRatings()
.ensure();
// Reversed index on content
posts.index()
.withContentDesceding()
.ensure();