Introduction

MongoDB bridges the gap between key-value stores (which are fast and highly scalable) and traditional RDBMS systems (which provide rich queries and deep functionality).

MongoDB (from "humongous") is a scalable, high-performance, open source, document-oriented database.

This project aims to provide an object-mapping layer on top of Mongo to ease common activities such as:

  • Marshalling from Mongo to Groovy/Java types and back again

  • Support for GORM dynamic finders, criteria and named queries

  • Session-managed transactions

  • Validating domain instances backed by the Mongo datastore

Compatibility with GORM for Hibernate

This implementation tries to be as compatible as possible with GORM for Hibernate. In general you can refer to the GORM documentation and the "Domain Classes" section of the reference guide (see the right nav) for usage information.

The following key features are supported by GORM for Mongo:

  • Simple persistence methods

  • Dynamic finders

  • Criteria queries

  • Named queries

  • Inheritance

  • Embedded types

  • Query by example

However, some features are not supported:

  • HQL queries

  • Composite primary keys

  • Many-to-many associations (these can be modelled with a mapping class)

  • Any direct interaction with the Hibernate API

  • Custom Hibernate user types (custom types are allowed with a different API)

There may be other limitations not mentioned here so in general it shouldn’t be expected that an application based on GORM for Hibernate will "just work" without some tweaking involved. Having said that, the large majority of common GORM functionality is supported.

Release Notes

Below are the details of the changes across releases:

8.0

  • TTL indexes via indexAttributes: [expireAfterSeconds: N] in the mapping DSL

  • Text and other special indexes via indexAttributes: [type: 'text']

  • In-place reconciliation of changed index options, and of a declared index name held by an index on other keys, with opt-in indexAttributes: [recreateOnConflict: true]

  • Index creation on startup can be switched off with grails.mongodb.buildIndexes = false, and run on demand with MongoDatastore.buildIndex(), or moved off the startup thread with grails.mongodb.buildIndexesAsync = true

  • grails.gorm and grails.mongodb settings are now applied when GORM is given an existing MongoClient, such as a MongoClient bean the application declares. Configuration that was previously ignored on this path — failOnError, the default mapping and constraints, flush behaviour and multi-tenancy — now takes effect. See Upgrade Notes before upgrading an application that supplies its own client.

  • The index build reports what it created, what was already present and how long it took, in one summary line per build

  • MongoDatastore.buildIndexAsync() runs the index build in the background whatever buildIndexesAsync says, and returns a CompletableFuture<IndexBuildResult> with the counts the summary line reports

  • MongoDatastore.findMissingIndexes() lists the indexes the domain classes declare that their collections do not have

  • MongoDatastore.findUndeclaredIndexes() lists the indexes on mapped collections that no domain class declares, and MongoDatastore.dropUndeclaredIndexes() drops them

  • A unique index that cannot be built over existing duplicate values is counted as a failed declaration and the index build goes on, instead of ending the build and, when it is synchronous, startup. See Upgrade Notes

  • Each connection builds the indexes of the domain classes mapped to it. Previously a named connection also created the indexes, and the collection, of every class mapped only to another connection in its database. See Upgrade Notes

  • A domain class registered after startup is indexed on the collection and in the database its mapping names, and only when it is mapped to the default connection. Previously its indexes were built on the collection named after the class, in the default database, whatever its mapping and connection

  • A property declared index: true in the mapping and also named in the constraints is indexed when the default constraints configure every property with '*'. Previously the constraints entry replaced the mapping’s, and the index was never created

  • A property configured after a '*' entry in a MappingBuilder.document { } mapping keeps its configuration, such as index: true. Previously each such property was configured on a copy of the '*' entry that was never kept, so the configuration was lost

  • Inside a withConnection block, static methods called on the class and instance methods such as save() now use the block’s connection, as documented; previously only the methods called without naming the class did

  • The same holds inside a session or transaction opened through a connection, such as Book.moreBooks.withTransaction { }, and inside a method annotated @Transactional(connection = 'moreBooks'): book.save() in it was written to the default connection

  • An application can be checkpointed and restored with CRaC, including as its context refreshes (spring.context.checkpoint=onRefresh). GORM opens no connection until the datastore starts, and around a checkpoint it stops and starts the MongoClient of every connection it created. In a Spring Boot application GORM now declares the MongoClient bean itself, built from Spring Boot’s settings, so that client is one of them. The client stays the same object, so the mongo bean keeps working after a restore. See Advanced Configuration.

7.1

  • Support Apache Groovy 3, and Java 14

  • Upgrade to mongodb-driver-sync 4.3.3

  • Autowire bean by type in the Data Service

  • Compatible only with Grails 5

7.0

  • Support for MongoDB Driver 3.10.0

  • Support for Java 11

  • Removal of RxJava 1.x Module

  • Java 8 Minimum

6.1

  • GORM Data Services Support

  • Package Scanning Constructors

  • Decimal128 Type Support

  • MongoDB 3.4.x Java Driver Support

6.0

  • Multiple Data Sources Support

  • Multi Tenancy Support

  • RxGORM for MongoDB (Using MongoDB Rx Drivers)

  • Unified Configuration model

5.0

  • MongoDB 3.x driver support

  • New Codec Persistence Engine

  • Removal of GMongo

  • Trait based

4.0

  • Grails 3 compatibility

3.0

  • Support for MongoDB 2.6

  • MongoDB 2.6 GeoJSON type support (MultiPoint, MultiLineString, MultiPolygon and GeometryCollection)

  • Support for Maps of embedded entities

  • Flexible index definition

  • Full text search support

  • Support for projections using MongoDB aggregation

  • Size related criteria implemented (sizeEq, sizeLt etc.) on collections

2.0

  • GeoJSON shape support

  • Support for SSL connections

  • Support for MongoDB connection strings

1.3

  • Support for stateless mode to improve read performance

  • Support for dynamically switching which database or collection to persist to at runtime

1.2

MongoDB plugin 1.2 and above requires Grails 2.1.5 or 2.2.1 as a minimum Grails version, if you are using older versions of Grails you will need to stay with 1.1

1.1 GA

  • DBRefs no longer used by default for associations

  • Upgrade to GMongo 1.0 and Spring Data MongoDB 1.1

  • Support for global mapping configuration

1.0 GA

  • Initial feature complete 1.0 release

Upgrade Notes

String id Storage Now Defaults to ObjectId

A domain that declares String id without its own storedAs mapping now persists _id as a BSON ObjectId. Before 8.0.0 the default was BSON String.

Application code is unaffected: person.id is still the 24-char hex String, and get, findAllByIdInList, updates and deletes still accept hex strings. What changes is the BSON type written to and matched in the database.

This is a data-compatibility change. An application upgrading from 7.x whose collections already hold String _id values has two options:

  1. Keep the previous behavior by pinning the old default:

    grails:
      mongodb:
        stringIds:
          defaultStoredAs: string
  2. Or migrate the stored _id values to ObjectId. Because _id is immutable, each document must be reinserted under the converted id rather than updated in place.

Domains keyed by a natural string value (slug, email, UUID) should declare the opt-out explicitly, since such values are not valid ObjectId hex:

class UserProfile {
    String id   // e.g. "jsmith@example.com"

    static mapping = {
        id generator: 'assigned', storedAs: String
    }
}

See Identity Generation for the full description of storedAs.

A Transaction Started Inside Another Joins It

A transactional service method called from another, or withTransaction inside withTransaction, used to begin a second transaction on the same session: it committed on its own, and the surrounding transaction’s commit then did nothing, so the surrounding transaction’s later writes could be lost. It now joins the surrounding transaction, and everything commits once when that one does, as on Hibernate.

The effect most applications will notice: without multi-document transactions, GORM for MongoDB does not flush before a query while a transaction is in progress, so a write a joined call leaves queued is not visible to a later query in the same transaction. It used to be, because the inner transaction flushed it when it returned:

@Transactional
void placeOrder(Order order) {
    orderService.save(order)                // @Transactional: now joins placeOrder's transaction, unflushed
    Order.countByCustomer(order.customer)   // does not see the order yet
}

Save with flush: true where a later query in the same transaction must see the write. This includes integration tests annotated with @Rollback, whose service calls now join the test’s transaction. With multi-document transactions enabled (grails.mongodb.transactional), GORM flushes before a query inside the server-side transaction, so the query sees the order, and the write is still rolled back if the transaction fails. See Nested Transactions for the other propagation changes.

Dependency Upgrades

GORM 7.1 supports Apache Groovy 3, Java 14, MongoDB Driver 4.3 and Spring 5.3.x.

Each of these underlying components may have changes that require altering your application. These changes are beyond the scope of this documentation.

Default Autowire By Type inside GORM Data Services

A Grails Service (or a bean) inside GORM DataService will default to autowire by-type, For example:

./grails-app/services/example/BookService.groovy

package example

import grails.gorm.services.Service

@Service(Book)
abstract class BookService {

    TestService testRepo

    abstract Book save(String title, String author)

    void doSomething() {
        assert testRepo != null
    }
}

Please note that with autowire by-type as the default, when multiple beans for same type are found the application with throw Exception. Use the Spring `@Qualifier annotation for Fine-tuning Annotation Based Autowiring with Qualifiers.

Indexes Are Built When the Datastore Starts, Not When It Is Created

Creating a MongoDatastore no longer connects to MongoDB or builds the indexes the domain classes declare, whichever constructor creates it, including one handed a MongoClient the application created itself. The datastore connects and builds them when it is started. In an application, Spring starts it with the other lifecycle beans, after every bean has been created and before BootStrap runs or the web server accepts a request, so the indexes are in place where they were before.

Code that creates a MongoDatastore itself and relies on the indexes existing once the constructor returns has to start it first:

MongoDatastore datastore = new MongoDatastore(configuration, Person)
datastore.start()      // connects, and builds the indexes Person declares

A datastore that nothing has started starts itself the first time a session is opened on it, which any GORM query, save or withTransaction does, so code that goes through GORM finds the indexes as before. What needs the explicit start() is code that inspects the indexes, or reaches MongoDB through the MongoClient directly, straight after creating the datastore. A datastore stopped with stop() is started again only by start(); using it in between is refused.

Code that queries MongoDB while the application’s beans are still being created, rather than in BootStrap or later, starts the datastore early. It still works, but the process can then no longer be checkpointed with CRaC as its context refreshes; see Checkpoint and Restore.

GORM Declares the MongoClient in a Spring Boot Application

In a Spring Boot application, grails-data-mongodb-spring-boot now declares the MongoClient bean itself, ahead of Spring Boot’s MongoDB auto-configuration, which then declares none. GORM builds it the way Spring Boot builds its own, from the spring.mongodb.* properties, Spring Boot’s MongoClientSettings and every Spring Boot MongoClientSettingsBuilderCustomizer bean, so it connects to the same server with the same options. Spring Data, health checks, metrics and anything else that injects a MongoClient are given GORM’s. GORM owns that client: it connects it when the datastore starts, closes it when the application shuts down, and stops and starts it around a CRaC checkpoint. Before, GORM’s auto-configuration ran after Spring Boot’s and was handed Spring Boot’s client, which GORM then left alone, so it was not stopped for a checkpoint.

An application that declares a MongoClient bean of its own keeps it, and GORM uses it as before.

Configuration Is Now Applied to an Externally-Supplied Client

When GORM is handed a MongoClient that it did not create — which is what happens whenever the application declares a MongoClient bean of its own — it built its settings from the defaults and applied only the database name. Everything else configured under grails.gorm and grails.mongodb was silently ignored on that path. Those settings are now applied, exactly as they always were when GORM creates the client itself.

Nothing in your configuration changes. What changes is that configuration you already have starts taking effect:

  • grails.gorm.failOnError — an invalid save() now throws ValidationException instead of returning null. An application that has had this set for years while running without it will start throwing on the first invalid save.

  • grails.gorm.default.mapping and grails.gorm.default.constraints — now applied to every domain class.

  • grails.gorm.autoFlush, grails.gorm.markDirty and grails.gorm.flushMode — now honoured.

  • grails.gorm.multiTenancy / grails.mongodb.multiTenancy — tenant discrimination and routing now follow the configured mode instead of NONE, so review your tenant resolver and your existing data before upgrading.

Multi-tenancy needs one extra step on this path. A TenantResolver registered as a Spring bean is injected into the connection source factory, and there is no factory involved when the client is supplied, so the bean is never consulted. Configure the resolver by class instead:

grails:
    gorm:
        multiTenancy:
            mode: DISCRIMINATOR
            tenantResolverClass: com.example.MyTenantResolver

Without it the mode is active but the resolver is NoTenantResolver, which throws TenantNotFoundException on every tenant-scoped operation. Settings under grails.mongodb continue to take precedence over the grails.gorm equivalents.

The connection details are the exception, since the supplied client is already connected: grails.mongodb.url is not applied to it. That also matters for a connection declared under grails.mongodb.connections without a url of its own, which falls back to the host, port, username and password settings — 127.0.0.1:27017 unless those are set — rather than to grails.mongodb.url. Give each such connection its own url.

Calls on a Class Inside a Named Connection’s Session or Transaction Use That Connection

Inside Book.moreBooks.withTransaction { }, Book.moreBooks.withNewSession { }, a withConnection block or a method annotated @Transactional(connection = 'moreBooks') for a class that declares that connection, the calls on Book itself, such as Book.list() or book.save(), now use that connection. Previously they used the default connection, so a document saved there was written to the default database. Code that reached the default connection from inside such a block by calling the class directly should name it, as in ``Book.default.list()``.

MongoClient Lifecycle for an Externally-Supplied Client

When you construct a MongoDatastore with a MongoClient that you created yourself, GORM no longer closes that client when the datastore shuts down. The client is owned by whoever created it, so closing it is now your responsibility.

This only affects you if you previously relied on MongoDatastore.close() to also close a client you passed in:

MongoClient mongoClient = MongoClients.create("mongodb://localhost/mydb")
MongoDatastore datastore = new MongoDatastore(mongoClient, Person)

// ...

datastore.close()    // no longer closes mongoClient
mongoClient.close()  // close the client you created

No change is required when GORM creates the client itself — for example new MongoDatastore(Person), or configuration-driven setups where GORM builds the client from grails.mongodb.* settings — because GORM continues to own and close those clients. A MongoClient registered as a Spring bean likewise continues to be closed by the ApplicationContext.

A Unique Index Over Duplicate Values No Longer Stops the Index Build

A declared unique index that cannot be built because documents in the collection already share a value used to end the index build with a DuplicateKeyException: the indexes declared after it were not built, and with the default synchronous build the application did not start. It is now handled like any other index the server refuses: logged at ERROR with the domain class and the duplicate key, counted as a failure in the build summary, which is logged at WARN, and the build goes on with the remaining declarations. The application starts without that index.

Remove the duplicates and build the index again, with buildIndex() or by restarting, to have it created. To keep refusing to run without it, check the result of buildIndexAsync() (see Building Indexes on Demand) and stop when failures() is not zero.

A Connection Builds Only the Indexes of the Classes Mapped to It

The index build of each connection created the indexes declared by every domain class, including classes mapped only to another connection, so a class mapped to the default connection also had its collection and indexes created in the database of every named connection. Each connection now builds the indexes of the classes mapped to it: those that name it with connection or connections, those mapped to ConnectionSource.ALL, and, on the default connection, those that name none. The indexes already created elsewhere stay where they are; findUndeclaredIndexes() on that connection does not report them, since no class is mapped to their collection there.

Getting Started

Basic Setup

To get started with GORM for MongoDB within Grails you need configure it as a dependency in build.gradle:

dependencies {
    compile 'org.grails.plugins:mongodb:8.0.0'
}

Common Errors

If you receive an error that indicates a failure to resolve the grails-datastore-simple dependency you may need to add the following to build.gradle directly above the dependencies block:

build.gradle
configurations.all {
    exclude module:'grails-datastore-simple'
}

If you receive an error at runtime such as:

Caused by: org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class org.bson.BsonDecimal128.
        at org.bson.codecs.configuration.CodecCache.getOrThrow(CodecCache.java:46)
        at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:63)
        at org.bson.codecs.configuration.ChildCodecRegistry.get(ChildCodecRegistry.java:51)
        at org.bson.codecs.BsonTypeCodecMap.<init>(BsonTypeCodecMap.java:44)
        at org.bson.codecs.BsonDocumentCodec.<init>(BsonDocumentCodec.java:65)

It means you have an older version of the MongoDB Java driver on your classpath and you should add the following declaration to build.gradle to ensure the dependency is correct:

build.gradle
compile "org.mongodb:mongodb-driver:5.12.0"

Configuring MongoDB

With that done you need to set up a running MongoDB server. Refer to the MongoDB Documentation for an explanation on how to startup a MongoDB instance. Once installed, starting MongoDB is typically a matter of executing the following command:

MONGO_HOME/bin/mongod

With the above command executed in a terminal window you should see output like the following appear:

2015-11-18T19:38:50.073+0100 I JOURNAL  <<initandlisten>> journal dir=/data/db/journal
2015-11-18T19:38:50.073+0100 I JOURNAL  <<initandlisten>> recover : no journal files present, no recovery needed
2015-11-18T19:38:50.090+0100 I JOURNAL  <<durability>> Durability thread started
2015-11-18T19:38:50.090+0100 I JOURNAL  <<journal writer>> Journal writer thread started
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> MongoDB starting : pid=52540 port=27017 dbpath=/data/db 64-bit host=Graemes-iMac.local
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> ** WARNING: You are running this process as the root user, which is not recommended.
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>>
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>>
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> db version v3.0.4
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> git version: 0481c958daeb2969800511e7475dc66986fa9ed5
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> build info: Darwin mci-osx108-11.build.10gen.cc 12.5.0 Darwin Kernel Version 12.5.0: Sun Sep 29 13:33:47 PDT 2013; root:xnu-2050.48.12~1/RELEASE_X86_64 x86_64 BOOST_LIB_VERSION=1_49
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> allocator: system
2015-11-18T19:38:50.090+0100 I CONTROL  <<initandlisten>> options: {}
2015-11-18T19:38:50.176+0100 I NETWORK  <<initandlisten>> waiting for connections on port 27017

As you can see the server is running on port 27017, but don’t worry the MongoDB plugin for Grails will automatically configure itself to look for MongoDB on that port by default.

If you want to configure how Grails connects to MongoDB then you can do so using the following settings in grails-app/conf/application.yml:

grails:
    mongodb:
        host: "localhost"
        port: 27017
        username: "blah"
        password: "blah"
        databaseName: "foo"

Using MongoDB Standalone

If you plan to use MongoDB as your primary datastore then you need to remove the Hibernate plugin from the build.gradle file by commenting out the hibernate line in the plugins block

compile 'org.grails.plugins:hibernate'

With this done all domain classes in grails-app/domain will be persisted via MongoDB and not Hibernate. You can create a domain class by running the regular create-domain-class command:

grails create-domain-class Person

The Person domain class will automatically be a persistent entity that can be stored in MongoDB.

Combining MongoDB and Hibernate

If you have both the Hibernate and Mongo plugins installed then by default all classes in the grails-app/domain directory will be persisted by Hibernate and not Mongo. If you want to persist a particular domain class with Mongo then you must use the mapWith property in the domain class:

static mapWith = "mongo"

Advanced Configuration

Mongo Database Connection Configuration

As mentioned the GORM for MongoDB plugin will configure all the defaults for you, but if you wish to customize those defaults you can do so in grails-app/conf/application.yml:

grails:
    mongodb:
        host: localhost
        port: 27017
        username: blah
        password: blah
        databaseName: foo

or equivalently in grails-app/conf/application.groovy:

grails {
    mongodb {
        host = "localhost"
        port = 27017
        username = "blah"
        password = "blah"
        databaseName = "foo"
    }
}
These settings are read by name, so write them exactly as they are documented. Unlike Spring Boot’s own configuration properties they are not relaxed-bound, and a kebab-case spelling such as database-name is not recognised — it is ignored, leaving the default in place.

The databaseName setting configures the default database name. If not specified the databaseName will default to the name of your application.

You can also customize the MongoDB connection settings using an options block:

grails {
    mongodb {
        options {
            autoConnectRetry = true
            connectTimeout = 300
        }
    }
}

Available options and their descriptions are defined in the MongoClientOptions javadoc.

MongoDB Connection Strings

Since 2.0, you can also use MongoDB connection strings to configure the connection:

grails {
    mongodb {
        url = "mongodb://localhost/mydb"
    }
}

Using MongoDB connection strings is currently the most flexible and recommended way to configure MongoDB connections.

Configuration Options Guide

Below is a complete example showing all configuration options:

grails {
    mongodb {
        databaseName = "myDb" // the default database name
        host = "localhost" // the host to connect to
        port = 27017 // the port to connect to
        username = ".." // the username to connect with
        password = ".." // the password to connect with
        stateless = false // whether to use stateless sessions by default
        buildIndexes = true // whether GORM creates the indexes declared in mapping blocks by itself on startup
        buildIndexesAsync = false // whether index builds run on a background thread instead of blocking the caller

        // Alternatively, using  'url'
        // url = "mongodb://localhost/mydb"

        options {
            connectionsPerHost = 10 // The maximum number of connections allowed per host
            threadsAllowedToBlockForConnectionMultiplier = 5
            maxWaitTime = 120000 // Max wait time of a blocking thread for a connection.
            connectTimeout = 0 // The connect timeout in milliseconds. 0 == infinite
            socketTimeout = 0 // The socket timeout. 0 == infinite
            socketKeepAlive = false // Whether or not to have socket keep alive turned on
            writeConcern = new com.mongodb.WriteConcern(0, 0, false) // Specifies the number of servers to wait for on the write operation, and exception raising behavior
            sslEnabled = false // Specifies if the driver should use an SSL connection to Mongo
            socketFactory = ... // Specifies the SocketFactory to use for creating connections
        }
    }
}

Global Mapping Configuration

Using the grails.mongodb.default.mapping setting in grails-app/conf/application.groovy you can configure global mapping options across your domain classes. This is useful if, for example, you want to disable optimistic locking globally or you wish to use DBRefs in your association mappings. For example, the following configuration will disable optimistic locking globally and use DBRefs for all properties:

grails.mongodb.default.mapping = {
    version false
    '*'(reference:true)
}

The * method is used to indicate that the setting applies to all properties.

The storage type of String id fields is controlled globally by grails.mongodb.stringIds.defaultStoredAs (values objectid, the default as of 8.0.0, or string). See Identity Generation for details, including the upgrade note for applications with existing String _id data.

Persistence Engine (deprecated)

grails.mongodb.engine selects the persistence engine. codec is the default and the recommended value. mapping remains available for compatibility, but is deprecated.

The mapping engine is deprecated and will be removed in a future release. It reaches MongoDB through a separate persister hierarchy that has to be kept in step with the codec one for every change to the storage layer, and it offers no capability the codec engine lacks. An application setting it should remove the setting:

grails:
  mongodb:
    engine: mapping   (1)
1 Deprecated. Remove this to use the default codec engine.

Selecting it logs a warning at startup.

Index Creation on Startup

GORM creates and reconciles the indexes declared in your mapping blocks each time the datastore starts, and startup waits for MongoDB to finish building them. In an application the datastore starts when Spring starts its lifecycle beans, after every bean has been created and before BootStrap runs or the web server accepts a request. A datastore created outside an application context starts the first time a session is opened on it. Set grails.mongodb.buildIndexes = false to leave the indexes on the server untouched instead, and build them with MongoDatastore.buildIndex() when you choose, or grails.mongodb.buildIndexesAsync = true to keep building them but on a background thread — see Querying Indexing for details and when to use each.

Checkpoint and Restore (CRaC)

GORM takes part in the Spring lifecycle, so an application can be checkpointed with CRaC and restored, whether the checkpoint is taken while the application is running or as its context refreshes (-Dspring.context.checkpoint=onRefresh). CRaC refuses to checkpoint a process holding open sockets, and a connected MongoClient holds several:

  • GORM opens no connection to MongoDB until the datastore starts. The clients it creates connect when the datastore is started, and the declared indexes are built then rather than while the datastore is being created. A checkpoint taken as the context refreshes, which Spring takes after every bean has been created and before it starts any lifecycle bean, therefore finds nothing to refuse, and the datastore connects once the process is restored.

  • Before a checkpoint of a running application, GORM stops the client of every connection: the default one, each one under grails.mongodb.connections, and any added at runtime. When the application is restored, GORM starts each of them again with the same settings. Nothing connects in between: a connection added, or a domain class registered, while the application is stopped is connected and has its indexes built when it is restored.

An embedded MongoDB is stopped after the clients and started again before them.

The MongoClient GORM hands out stays the same object across a checkpoint: stopping it closes the driver’s client inside it, and starting it builds a new one. The mongo bean, a client obtained from MongoDatastore.getMongoClient(), multi-tenancy and the Spring Data MongoDB integration all go on working after the restore. A MongoDatabase, MongoCollection or ClientSession obtained from the client belongs to the driver’s client it came from, so obtain it again after a restore rather than keeping it.

A client the application supplied to GORM is neither started, stopped nor replaced. It belongs to whoever created it, and that code has to close it before the checkpoint for the checkpoint to succeed, and, for a checkpoint taken as the context refreshes, must not have connected it by then. In a Spring Boot application GORM declares the MongoClient bean itself, built from Spring Boot’s settings (see Using GORM in Spring Boot), so unless the application declares one of its own, that client is GORM’s and is handled like the rest.

A checkpoint taken as the context refreshes also needs the application itself to leave MongoDB alone until then: code that queries it while beans are still being created, rather than in BootStrap or later, connects the datastore early, and so does reading the connections themselves from MongoDB with MongoConnectionSources, which reads them as the datastore is created.

Multi-Document Transactions

By default a GORM transaction for MongoDB is a client-side unit of work: pending inserts, updates and deletes are batched and flushed to the server when the transaction commits, but each write is applied individually and is not rolled back if a later operation fails.

Since MongoDB 4.0, the server supports multi-document ACID transactions. To have GORM run transactional operations inside a real server-side transaction — so that all writes within a withTransaction block (or a @Transactional service method) commit or roll back atomically — enable:

grails {
    mongodb {
        transactional = true
    }
}

With this enabled, a GORM transaction starts a MongoDB session and transaction; on commit all buffered writes are committed together, and on rollback they are discarded on the server:

Person.withTransaction {
    new Person(name: "Fred").save()
    new Person(name: "Wilma").save()
    // both inserts commit together, or neither is applied if an exception is thrown
}

A transaction started inside another joins it, so the writes of both commit or roll back together; PROPAGATION_REQUIRES_NEW runs in a server-side transaction of its own. A read-only transaction reads without one. See Nested Transactions and Read-Only Transactions.

Multi-document transactions require a replica set or a sharded cluster; they are not supported on a standalone mongod. If transactional is enabled but a standalone topology is detected, GORM logs a warning once and falls back to the default client-side flush behavior. Identifier generation for native (Long) identity uses an independent counter and is intentionally not enrolled in the transaction, mirroring the non-transactional semantics of database sequences.
A per-transaction timeout is not supported for MongoDB server-side transactions. The session and transaction are started before the transaction manager applies a timeout, so a timeout on the withTransaction or @Transactional definition cannot be honored. Rather than silently ignore it, GORM throws a TransactionUsageException when a non-default timeout is requested. The server enforces its own maximum transaction duration via transactionLifetimeLimitSeconds (60 seconds by default).
Retry Semantics

MongoDB labels two categories of transaction errors as retryable, and GORM deliberately handles only one of them:

UnknownTransactionCommitResult

The outcome of the commit is unknown — for example a network failure or a replica set election occurred while the commit was in flight. The commit may already have succeeded on the server, and MongoDB guarantees that re-issuing it is safe and will not apply the writes twice. GORM therefore retries the commit automatically (up to three times) before propagating the error.

TransientTransactionError

The entire transaction has failed — for example a write conflict with a concurrent transaction — and can only be recovered by re-executing everything inside it. GORM intentionally does not retry these. Re-running the transaction body would also re-run any side effects it contains (sending email, calling external services, publishing messages), which is only safe when the body is idempotent — something only the application can know. The exception is propagated and whole-transaction retry is left to application code, matching the behavior of Spring Data MongoDB’s MongoTransactionManager.

Applications whose transaction body is safe to re-run can implement the retry themselves by checking for the TransientTransactionError label:

import com.mongodb.MongoException

int attempts = 0
while (true) {
    try {
        Person.withTransaction {
            // transactional work that is safe to re-run
        }
        break
    }
    catch (MongoException e) {
        if (e.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL) && ++attempts < 3) {
            continue
        }
        throw e
    }
}

Declarative retry libraries such as Spring Retry (@Retryable) can be used instead of a manual loop, applied around the transactional method.

Embedded MongoDB

grails-data-mongodb-embedded starts a MongoDB server as the application starts, so development and testing need neither a MongoDB installation nor Docker. Applications created by Grails Forge with MongoDB selected include it, asked for by the development and test environments.

A server is asked for by the URL, the way an in-memory SQL database is. Where an application would name a host, it names embedded instead:

implementation "org.apache.grails:grails-data-mongodb-embedded"
environments:
    development:
        grails:
            mongodb:
                url: mongodb://embedded/bookstore
    test:
        grails:
            mongodb:
                url: mongodb://embedded/bookstore
    production:
        grails:
            mongodb:
                url: mongodb://localhost:27017/bookstore

Nothing else switches it on and nothing switches it off. A URL naming a host is served by the driver and no server is started, so adding the dependency alone never changes how an application connects — in the same way that an application with h2 on its classpath and a PostgreSQL URL never starts H2.

A port may be given as mongodb://embedded:27018/bookstore. Without one, the port is 27017 offset by however far server.port has moved from 8080, so two applications run side by side without colliding.

A host genuinely named embedded cannot be reached this way. Name it by its address or its fully qualified name instead.

Choosing a backend

Two backends are supported.

in-memory flapdoodle

Included

Yes, with this module

No, add de.flapdoodle.embed:de.flapdoodle.embed.mongo

Startup

Milliseconds, inside this JVM

About a second, after downloading a MongoDB binary once to ~/.embedmongo

Fidelity

Reimplements the wire protocol, so transactions, change streams, $text and some $expr operators are unsupported

A real mongod, so everything behaves as it does in production

Keeps data

No

Yes, with embedded.mongodb.database-dir

Reported version

5.0.0

The version started, 8.0 by default

in-memory is used unless flapdoodle is on the classpath, in which case flapdoodle is preferred, so adding the dependency is all that is needed to move to a real mongod. Set embedded.mongodb.backend to choose explicitly.

Flapdoodle is not a dependency of this module because it requires jgrapht, which is offered under LGPL-2.1 or EPL-2.0 and so cannot be required by an Apache release. An application adds it in the same way it chooses a SQL driver:

implementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.33.0"

Configuration

Whether to start a server, which port, and which database are all read from the URL. These configure the server behind it.

Property Description

embedded.mongodb.backend

in-memory or flapdoodle. Defaults to flapdoodle when it is on the classpath, otherwise in-memory.

embedded.mongodb.database-dir

Where to keep the data so it outlives the server. Only the flapdoodle backend supports this; the in-memory backend reports an error rather than discarding the data silently.

embedded.mongodb.version

The MongoDB version for backends that can choose one, such as V8_0 for flapdoodle.

embedded.mongodb.replica-set

The name of the replica set to run as, such as rs0. Unset, the server is standalone unless grails.mongodb.transactional is true, which asks for one implicitly.

embedded.mongodb.property-names

Comma separated properties that may name an embedded server. Defaults to grails.mongodb.url.

Transactions

A multi-document transaction, a change stream and a causally consistent read are all refused by a standalone MongoDB server: every one of them needs a replica set. An application that asks GORM for transactions is therefore given one - a set of a single node, which is what a single machine has to offer, and enough for all three:

grails:
    mongodb:
        url: mongodb://embedded/bookstore
        transactional: true

The server runs as the replica set rs0 and is handed to the application only once it has elected itself primary, so the first query does not race the election. Name the set with embedded.mongodb.replica-set to call it something else, or to run one without asking GORM for transactions:

embedded:
    mongodb:
        replica-set: rs0

Only the flapdoodle backend can do this, because only a real mongod has replication. The in-memory backend reimplements the wire protocol and refuses transactions whatever it is configured with.

A replica set is initiated by a command a driver sends rather than by a setting, so org.mongodb:mongodb-driver-sync has to be on the classpath. Any application talking to MongoDB already has it; a standalone embedded server does not need it.

Production

A generated application names a real host in production, so nothing is started there.

Where an embedded server in production is genuinely wanted, ask for one in that environment’s url and use flapdoodle with a directory, so the data survives a restart:

environments:
    production:
        grails:
            mongodb:
                url: mongodb://embedded/bookstore
        embedded:
            mongodb:
                backend: flapdoodle
                database-dir: ./prodMongoDb

Restarts and ports

A server started by this module runs until the JVM exits, so a Spring Boot DevTools restart reuses it instead of starting another. Only a server this module started is reused: if anything else is already holding the port, startup fails with an error naming the port rather than connecting to an unrelated service.

A restart that carries a different setting - a backend, a database directory, a version, a replica set, or transactions being switched on - is given a server that matches it, which means the previous one is replaced and anything it held in memory goes with it. Data in a database-dir outlives the replacement, except across a change of version: MongoDB refuses to start against a directory written by a different release. To use a MongoDB started by hand, name its host in grails.mongodb.url as usual.

Switching backend counts as much as the rest: an application handed the in-memory server after asking for flapdoodle would not fail at the switch but later, at the first transaction or $text query it ran. Naming a backend whose library is not on the classpath fails the restart rather than handing back the server already listening, which is what that configuration does when nothing is running yet.

Checkpointing as the context refreshes

The server is normally started as soon as the application context is initialized, so that anything talking to MongoDB while beans are still being created finds it listening. An application checkpointed with CRaC as its context refreshes (-Dspring.context.checkpoint=onRefresh), or one that exits there (-Dspring.context.exit=onRefresh, as a run that trains a class data sharing archive does), has its URL published all the same, but its server is started with the application context’s lifecycle beans, ahead of the datastore. A checkpoint cannot contain the server’s listening socket, and a JVM halted as its context refreshes would leave a flapdoodle mongod running. A URL that asks for port 0 is given a free port when it is published, since the server binds later.

Outside Grails

The module is plain Java and knows nothing about Grails beyond the default property name, so embedded.mongodb.property-names makes it usable from any Spring application:

spring:
    data:
        mongodb:
            uri: mongodb://embedded/bookstore
embedded:
    mongodb:
        property-names: spring.data.mongodb.uri

Spring Data MongoDB applications are usually better served by flapdoodle’s own de.flapdoodle.embed.mongo.spring3x auto configuration, which this module deliberately does not duplicate.

Using GORM in Spring Boot

To use GORM for MongoDB in Spring Boot add the necessary dependencies to your Boot application:

compile("org.apache.grails:grails-data-mongodb-spring-boot:8.0.0")

Ensure your Boot Application class is annotated with ComponentScan, example:

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.*

@Configuration
@EnableAutoConfiguration
@ComponentScan
class Application {
    static void main(String[] args) {
        SpringApplication.run Application, args
    }
}
Using ComponentScan without a value results in Boot scanning for classes in the same package or any package nested within the Application class package. If your GORM entities are in a different package specify the package name as the value of the ComponentScan annotation.

GORM declares the application’s MongoClient bean, ahead of Spring Boot’s MongoDB auto-configuration, which then declares none. It is built the way Spring Boot builds its own: from the spring.mongodb.* properties, Spring Boot’s MongoClientSettings and every Spring Boot MongoClientSettingsBuilderCustomizer bean, so a @ServiceConnection, an SSL bundle or a metrics customizer applies to it as it would to Spring Boot’s. Spring Data, health checks and anything else that injects a MongoClient share GORM’s. GORM owns the client: it connects when the datastore starts, is closed when the application shuts down, and can be checkpointed with CRaC (see Checkpoint and Restore). An application that declares a MongoClient bean of its own keeps it, and GORM uses that client instead and leaves closing it to the application.

Finally create your GORM entities and ensure they are annotated with grails.persistence.Entity:

import grails.persistence.*

@Entity
class Person {
    String firstName
    String lastName
}

GORM for MongoDB without Grails

If you wish to use GORM for MongoDB outside of a Grails application you should declare the necessary dependencies, for example in Gradle:

compile "org.grails:grails-datastore-gorm-mongodb:8.0.0"

Then annotate your entities with the grails.gorm.annotation.Entity annotation:

@Entity
class Person {
    String name
}

Then you need to place the bootstrap logic somewhere in the loading sequence of your application which uses MongoDatastore:

def datastore = new MongoDatastore(Person)

println Person.count()

You can also supply your own com.mongodb.client.MongoClient if you need to configure the driver directly:

MongoClient mongoClient = MongoClients.create("mongodb://localhost/mydb")
def datastore = new MongoDatastore(mongoClient, Person)

println Person.count()
When you supply your own MongoClient, GORM does not close it when the datastore shuts down — the client is owned by whoever created it, so you are responsible for closing it. When GORM creates the client itself (for example new MongoDatastore(Person)), it closes it for you.

For configuration you can either pass a map or an instance of the org.springframework.core.env.PropertyResolver interface:

def initializer = new MongoDatastore(['grails.mongodb.url':'http://myserver'], Person)

println Person.count()

If you are using Spring with an existing ApplicationContext you can instead call MongoDbDataStoreSpringInitializer.configureForBeanDefinitionRegistry prior to refreshing the context. You can pass the Spring Environment object to the constructor for configuration:

ApplicationContext myApplicationContext = ...
def initializer = new MongoDbDataStoreSpringInitializer(myApplicationContext.getEnvironment(), Person)
initializer.configureForBeanDefinitionRegistry(myApplicationContext)

println Person.count()

Spring Data MongoDB Interoperability

The optional grails-data-mongodb-spring-data module lets an application use GORM for MongoDB and Spring Data MongoDB (its MongoTemplate and repositories) side by side, sharing one MongoClient, one database and one transaction. Add the dependency:

implementation "org.apache.grails:grails-data-mongodb-spring-data:8.0.0"

When the module and Spring Data MongoDB are on the classpath of a Spring Boot application that already has a GORM MongoDatastore, it auto-configures, over GORM’s existing connection:

  • a MongoDatabaseFactory bound to GORM’s MongoClient and default database (GORM keeps ownership of the client; the factory will not close it, and it goes on working across a CRaC checkpoint and restore),

  • a MongoTemplate (named mongoTemplate) and its MappingMongoConverter, sharing the driver-level codec registry,

  • a primary transactionManager that lets GORM and Spring Data participate in a single MongoDB transaction.

Spring Data repositories are enabled the usual way, in a package separate from your GORM @Entity classes:

@EnableMongoRepositories(basePackages = "com.example.repositories")
class Application {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}

Shared transactions

With GORM multi-document transactions enabled (grails.mongodb.transactional = true, see Advanced Configuration), a single transactional method commits or rolls back GORM writes and Spring Data writes together on one server-side ClientSession:

@Transactional
void transfer(MongoTemplate mongoTemplate) {
    new Account(name: "from").save()                 // GORM
    mongoTemplate.insert(new LedgerEntry(amount: 10)) // Spring Data
    // both commit together, or neither is applied if an exception is thrown
}

The unified manager is registered as the primary transactionManager (it has to be, to be chosen over GORM’s plain mongoTransactionManager for an unqualified @Transactional). It backs off automatically if the application already defines a bean named transactionManager. To run a block with GORM-only semantics, qualify the manager: @Transactional("mongoTransactionManager").

Because it is primary, in an application that mixes this with another persistence stack (for example Hibernate/JPA), an unqualified @Transactional resolves to this MongoDB manager. If you run multiple persistence stacks, define your own primary transactionManager (or qualify every @Transactional explicitly) so each method targets the intended manager.
On the unified manager, readOnly only governs GORM’s flush. A GORM write queued inside a read-only transaction is not flushed (see Read-Only Transactions), but a Spring Data write such as mongoTemplate.insert(...) is still persisted: a read-only transaction has no server-side transaction, so the write is applied on its own. A read-only transaction does not prevent Spring Data writes.
Propagation is as for GORM’s own transaction manager (see Nested Transactions), for Spring Data as for GORM: a transactional call inside another joins it, and REQUIRES_NEW suspends it and runs in a GORM session and ClientSession of its own, which its MongoTemplate calls use. A transaction begun inside withNewSession likewise runs its MongoTemplate calls in its own ClientSession, or without one if it is read-only, and the surrounding transaction’s calls go back to that transaction’s when it completes. NESTED is not supported.

Boundary

Only the connection, codecs and (within a transaction) the ClientSession are shared. The two object-mapping models remain separate: GORM maps its @Entity classes through its own mapping context, and Spring Data maps its own document classes through MappingMongoConverter. Do not point @EnableMongoRepositories at GORM @Entity packages, and do not register GORM entities with Spring Data’s mapping context. Two classes may map the same collection only if you keep their field mappings compatible.

Mapping Domain Classes

Basic Mapping

The way GORM for MongoDB works is to map each domain class to a Mongo collection. For example given a domain class such as:

class Person {
    String firstName
    String lastName
    static hasMany = [pets:Pet]
}

This will map onto a MongoDB Collection called "person".

Embedded Documents

It is quite common in MongoDB to embed documents within documents (nested documents). This can be done with GORM embedded types:

class Person {
    String firstName
    String lastName
    Address address
    static embedded = ['address']
}

You can map embedded lists and sets of documents/domain classes:

class Person {
    String firstName
    String lastName
    Address address
    List otherAddresses
    static embedded = ['address', 'otherAddresses']
}

You can also embed maps of embedded classes where the keys are strings:

class Person {
    String firstName
    String lastName
    Map<String,Address> addresses
    static embedded = ['addresses']
}

Basic Collection Types

You can also map lists and maps of basic types (such as strings) simply by defining the appropriate collection type:

class Person {
    List<String> friends
    Map pets
}

...

new Person(friends:['Fred', 'Bob'], pets:[chuck:"Dog", eddie:'Parrot']).save(flush:true)

Basic collection types are stored as native ArrayList and BSON documents within the Mongo documents.

Customized Collection and Database Mapping

You may wish to customize how a domain class maps onto a MongoCollection. This is possible using the mapping block as follows:

class Person {
    ..
    static mapping = {
        collection "mycollection"
        database "mydb"
    }
}

In this example we see that the Person entity has been mapped to a collection called "mycollection" in a database called "mydb".

You can also control how an individual property maps onto a Mongo Document field (the default is to use the property name itself):

class Person {
    ..
    static mapping = {
        firstName attr:"first_name"
    }
}

If you are using the mapping engine, for non-embedded associations by default GORM for MongoDB will map links between documents using MongoDB database references also known as DBRefs.

If you prefer not to use DBRefs then you tell GORM to use direct links by using the reference:false mapping:

class Person {
    ..
    static mapping = {
        address reference:false
    }
}

Identity Generation

By default in GORM entities are supplied with an integer-based identifier. So for example the following entity:

class Person {}

Has a property called id of type java.lang.Long. In this case GORM for Mongo will generate a sequence based identifier using the technique described in the Mongo documentation on Atomic operations.

However, sequence based integer identifiers are not ideal for environments that require sharding (one of the nicer features of Mongo). Hence it is generally advised to use either String based ids:

class Person {
    String id
}

Or a native BSON ObjectId:

import org.bson.types.ObjectId

class Person {
    ObjectId id
}

BSON ObjectId instances are generated in a similar fashion to UUIDs.

Native Identity Types

Declaring String id on every domain class ties that source to MongoDB: the same class compiled against Hibernate would need Long id instead. Where domain classes are shared, or an application is written to run against either store, the build can ask for the identity type of whichever GORM implementation each domain class is mapped with:

build.gradle
grails {
    gorm {
        defaultIdType = 'native'
    }
}

A domain class mapped with MongoDB is then given a String id — holding the hexadecimal form of a generated ObjectId, exactly as if it had been declared — while one mapped with Hibernate keeps Long. Nothing in the domain class itself names a store.

The default is defaultIdType = 'long', which gives every domain class a Long id as earlier releases did. The type is resolved when the domain class is compiled, from its mapWith property and the GORM implementations on the compilation classpath, so an application using more than one implementation gets the right type for each domain class from the single setting. A domain class that declares an id of its own keeps the type it declares either way.

Precompiled plugin domain classes cannot have their bytecode changed by the consuming application’s compiler. A plugin that wants to leave this choice to its consumer can declare a portable identity:

class Book {
    Serializable id
}

The consuming application uses the same build setting above for both cases. The Grails Gradle plugin packages its value as grails.gorm.defaultIdType, which is used at runtime for precompiled plugin domains. No duplicate application configuration is required, although normal external configuration can override the packaged value.

MongoDB resolves the portable identity to String for native and Long for long. The result is stored in the entity mapping metadata, so normal persistence operations do not perform repeated type resolution. An explicitly declared concrete identity type still wins.

This changes the type of a field that already holds data. An existing MongoDB application whose domain classes rely on the default has sequence-generated Long _id values stored, and turning this on makes new documents use ObjectId hex strings instead. Choose it when an application is written, or migrate what has already been stored.
The setting reaches the compiler through the Grails Gradle plugin, so it applies to Gradle builds. Compiling the same sources with an IDE configured to bypass Gradle produces Long ids.

Assigned Identifiers

Note that if you manually assign an identifier, then you will need to use the insert method instead of the save method, otherwise GORM can’t work out whether you are trying to achieve an insert or an update. Example:

class Person {
    String id
}
...
Person p = new Person()
p.id = "Fred"
// to insert
p.insert()
// to update
p.save()

Decoupling the Declared Type from the Storage Type

A domain that declares String id is convenient in application code — HTTP controllers return clean JSON, URLs embed ids directly, and you never need new ObjectId(...) at call sites. Writing _id as a BSON String to match would give up the native ObjectId benefits at the storage layer (12-byte index entries vs. 24-byte hex strings, embedded creation timestamp, etc.).

The storedAs mapping option decouples the two, so you get both: the domain sees a String id (hex form), while _id on disk is a BSON ObjectId. As of Grails 8.0.0 this is the default for any String id that declares no storedAs of its own — the mapping below is what that default applies for you, and is only needed to be explicit or to override a global defaultStoredAs: string:

import org.bson.types.ObjectId

class Person {
    String id

    static mapping = {
        id storedAs: ObjectId
    }
}

With storedAs: ObjectId:

  • _id is written as BSON ObjectId (native index size, sort order, creation timestamp)

  • person.id in application code is the 24-char hex string (clean JSON, no conversion at boundaries)

  • Person.get(hexString), findAllByIdInList([hexA, hexB]), updates, and deletes all coerce the declared-type value to the storage type automatically

  • Existing documents that already have BSON ObjectId _id values continue to load without a data migration

This is especially useful when migrating a legacy ObjectId id domain to String id for ergonomic reasons, because neither a data migration nor any change in application call sites is required.

Global Default

As of Grails 8.0.0 this is the default: every domain declaring String id that does not specify its own storedAs stores _id as a BSON ObjectId. No configuration is required to get the behavior described above.

To opt the whole application back out — for example an existing application whose data already holds BSON String _id values — pin the previous default in application.yml:

grails:
  mongodb:
    stringIds:
      defaultStoredAs: string

Valid values are objectid (BSON ObjectId, the default) and string (BSON String, the pre-8.0.0 default). Unrecognized values fall back to the default with a warning logged at startup.

This default changed in 8.0.0. An application upgrading from 7.x with existing String id data is reading and writing BSON String _id values; on 8.0.0 those documents are no longer matched by id lookups unless you either migrate the stored _id values to ObjectId or set defaultStoredAs: string above.

Per-domain storedAs always wins over the global default. Domains using a natural string key (slug, email, UUID) should opt out explicitly:

class UserProfile {
    String id   // e.g. "jsmith@example.com"

    static mapping = {
        id generator: 'assigned', storedAs: String
    }
}
Caveats
  • storedAs is currently honored only for converting between String and ObjectId. Other combinations are accepted but behave as if storedAs were unset.

  • Combining generator: 'assigned' with storedAs: ObjectId requires that the assigned value be a valid 24-char ObjectId hex string. Invalid values fall back to writing BSON String on the assumption that the domain is using a natural key despite the storedAs setting — consider declaring storedAs: String (or omitting it) in that case.

Understanding Dirty Checking

In order to be as efficient as possible when it comes to generating updates GORM for MongoDb will track changes you make to persistent instances.

When an object is updated only the properties or associations that have changed will be updated.

You can check whether a given property has changed by using the hasChanged method:

if( person.hasChanged('firstName') ) {
   // do something
}

This method is defined by the org.grails.datastore.mapping.dirty.checking.DirtyCheckable trait.

In the case of collections and association types GORM for MongoDB will wrap each collection in a dirty checking aware collection type.

Assigning a new collection over a tracked one keeps dirty checking active: the generated setter wraps the replacement in the same dirty checking aware type, so a pattern such as re-initialising an empty collection (if (!book.authors) book.authors = []) followed by in-place mutation is tracked and persisted. This holds for basic collections and maps, embedded collections, and one-to-many and many-to-many associations alike. The wrappers also track iterator-based mutation — Groovy’s removeAll(Closure) / retainAll(Closure), removeIf, ListIterator operations, sort / replaceAll, addFirst / addLast on a List property, removeFirst / removeLast on a List or SortedSet property, and the subList, reversed, headSet, tailSet and subSet views — and for Map properties the default methods (putIfAbsent, merge, compute*, replace*) and removals through the entrySet() / keySet() / values() views.

Assigning a collection that belongs to another entity re-binds it to the entity being assigned to, so bookA.tags = bookB.tags followed by bookA.tags.add('classic') marks bookA dirty rather than bookB. The two properties go on sharing the same underlying collection afterwards, exactly as a plain Groovy assignment would. This applies to the properties GORM wraps in a dirty checking aware collection — basic collections and maps, and embedded collections. When borrowed from another entity a one-to-many or many-to-many behaves differently: it is held in a PersistentCollection, which is stored as-is and left to the store, as with the store-specific types below. Re-initialising such a property on its own entity is still tracked, as described above.

A few paths remain outside automatic dirty checking and still require an explicit markDirty(propertyName):

  • a hand-written setter (one you define yourself rather than the generated one) stores the value it is given without re-wrapping, so a collection assigned through it loses tracking until the next load;

  • mutating an entry’s value directly during map iteration (Map.Entry.setValue);

  • replacing a collection wrapped by a store-specific type (for example the Neo4j collection wrappers) is deliberately left to that store’s persister, which re-wraps the raw replacement on save — that store’s behaviour is unchanged.

If any of your updates are not updating the properties that you anticipate you can force an update using the markDirty() method:

person.markDirty('firstName')

This will force GORM for MongoDB to issue an update for the given property name.

Dirty Checking and Proxies

Dirty checking uses the equals() method to determine if a property has changed. In the case of associations, it is important to recognize that if the association is a proxy, comparing properties on the domain that are not related to the identifier will initialize the proxy, causing another database query.

If the association does not define equals() method, then the default Groovy behavior of verifying the instances are the same will be used. Because proxies are not the same instance as an instance loaded from the database, which can cause confusing behavior. It is recommended to implement the equals() method if you need to check the dirtiness of an association. For example:

class Author {
    Long id
    String name

     /**
     * This ensures that if either or both of the instances
     * have a null id (new instances), they are not equal.
     */
    @Override
    boolean equals(o) {
        if (!(o instanceof Author)) return false
        if (this.is(o)) return true
        Author that = (Author) o
        if (id !=null && that.id !=null) return id == that.id
        return false
    }
}

class Book {
    Long id
    String title
    Author author
}

Querying Indexing

Basics

MongoDB doesn’t require that you specify indices to query, but like a relational database without specifying indices your queries will be significantly slower.

With that in mind it is important to specify the properties you plan to query using the mapping block:

class Person {
    String name
    static mapping = {
        name index:true
    }
}

With the above mapping a MongoDB index will be automatically created for you. You can customize the index options using the indexAttributes configuration parameter:

class Person {
    String name
    static mapping = {
        name index:true, indexAttributes: [unique:true, dropDups:true]
    }
}

You can use MongoDB Query Hints by passing the hint argument to any dynamic finder:

def people = Person.findByName("Bob", [hint:[name:1]])

Or in a criteria query using the query "arguments" method

Person.withCriteria {
        eq 'firstName', 'Bob'
    arguments hint:[1][firstName] }

Compound Indices

MongoDB supports the notion of compound keys. GORM for MongoDB enables this feature at the mapping level using the compoundIndex mapping:

class Person {
    ...
    static mapping = {
        compoundIndex name:1, age:-1
    }
}

As per the MongoDB docs 1 is for ascending and -1 is for descending.

TTL Indexes

MongoDB can automatically remove documents from a collection once they reach a certain age using a TTL index. Declare one by adding expireAfterSeconds to the indexAttributes of a single date-valued property:

class LoginEvent {
    Date dateCreated
    static mapping = {
        dateCreated index:true, indexAttributes: [expireAfterSeconds: 3600]
    }
}

With the mapping above MongoDB expires each LoginEvent roughly one hour (3600 seconds) after the value stored in its dateCreated property. The indexed property must hold a Date (or an array of dates), and TTL is supported on single-field indexes only — MongoDB ignores expireAfterSeconds if it is applied to a compoundIndex.

If you later change the declared expireAfterSeconds, GORM reconciles the existing index in place on startup (using MongoDB’s collMod command) so that the new expiry takes effect without dropping or rebuilding the index.

Text Indexes

In addition to declaring a text index with the index method (see Full Text Search), you can declare one on a single property through indexAttributes using type: 'text':

class Product {
    String title
    static mapping = {
        title index:true, indexAttributes: [type: 'text']
    }
}

The type attribute sets the index type for that field, so it can equally be used to declare other special index types such as '2d' or '2dsphere'. Note that MongoDB allows at most one text index per collection. See Full Text Search for how to query a text index once it has been created.

Reconciling Index Option Changes

An index is created from its declared options the first time the collection is initialised. If you later change the options of an already-created index — for example marking it unique — MongoDB cannot alter most options in place and reports an IndexOptionsConflict. By default GORM logs this conflict and leaves the existing index untouched.

To have GORM drop the existing index and recreate it with the newly declared options, add recreateOnConflict: true to the indexAttributes:

class Person {
    String name
    static mapping = {
        name index:true, indexAttributes: [unique:true, recreateOnConflict:true]
    }
}
Dropping and recreating an index rebuilds it from scratch, during which queries that rely on the index are not served by it. Enable recreateOnConflict deliberately, and prefer applying it during a maintenance window for large collections. The existing index is dropped first, so if the new one cannot be built — a unique index over documents that already share a value, say — the collection is left with no index on those keys. GORM logs that it dropped the index and could not build it again, and counts the declaration as failed.

A declared index whose name is already held by an index on different keys is the same kind of conflict, reported by MongoDB as an IndexKeySpecsConflict, and recreateOnConflict resolves it the same way:

class Person {
    String name
    static mapping = {
        name index:true, indexAttributes: [name:'byName', recreateOnConflict:true]
    }
}

Without it, GORM says which name is taken, leaves the index that holds it alone, and counts the declaration as failed.

A change to a TTL index’s expireAfterSeconds is handled automatically and does not require recreateOnConflict, because GORM updates the expiry in place rather than rebuilding the index.

Disabling Index Creation on Startup

By default GORM creates and reconciles every index declared in a mapping block when the datastore starts. Set buildIndexes to false in grails-app/conf/application.yml to switch that off:

grails:
    mongodb:
        buildIndexes: false

or, in application.groovy:

grails {
    mongodb {
        buildIndexes = false
    }
}

With this setting GORM issues no createIndex or collMod command by itself, for any domain class, and the indexes already present on the server are left exactly as they are until the application builds them (see Building Indexes on Demand below). Queries are unaffected and continue to use whichever indexes exist. This is useful when deploying against live data whose indexes are managed separately — by a DBA or a migration step — so that a deployment does not build an index against a large production collection, and so an application running against an older index set does not have those indexes reconciled underneath it.

It also suppresses index creation for domain classes registered after startup. Declared at the top level the setting applies to every connection, and each connection can override it:

grails:
    mongodb:
        buildIndexes: false
        connections:
            reporting:
                url: mongodb://localhost/reporting
                buildIndexes: true
Because nothing is created at startup, a collection that has never been built with buildIndexes enabled, or by an explicit build, has no declared indexes at all. Turn the setting off only where the indexes are already in place or are applied by other means. The setting governs only the indexes GORM derives from the mapping blocks; an explicit createIndex call made by application code against a collection is unaffected.
Building Indexes on Demand

Turning buildIndexes off stops GORM building indexes by itself. It does not stop the application building them when it chooses: once a deployment has been verified, from an administrative action, or on a schedule. Call buildIndex() on the MongoDatastore bean:

import org.grails.datastore.mapping.mongo.MongoDatastore

class IndexMaintenanceService {

    MongoDatastore mongoDatastore

    void buildIndexes() {
        mongoDatastore.buildIndex()
    }
}

This creates and reconciles the declared indexes exactly as the startup build would, and logs the same summary. It blocks the calling thread until MongoDB has built every index, unless buildIndexesAsync is enabled, in which case it returns at once and the build runs on the background thread. Each named connection builds its own domain classes, so build those through that connection’s datastore:

mongoDatastore.getDatastoreForConnection('reporting').buildIndex()

A build requested after the datastore has been closed is not started, and one requested while it is stopped for a checkpoint runs when it is restarted.

To build in the background and still learn how the build went, call buildIndexAsync(). It runs the same build on the connection’s index build thread whatever buildIndexesAsync says, returns at once, and hands back a CompletableFuture<IndexBuildResult>:

import org.grails.datastore.mapping.mongo.IndexBuildResult

IndexBuildResult result = mongoDatastore.buildIndexAsync().get()
println "${result.created()} created, ${result.alreadyPresent()} already present, ${result.failures()} failed"

The result carries the counts the summary line reports, and created and already-present indexes are always told apart, whatever the log level. A declaration that cannot be applied is counted in failures() and logged as it fails, and the future still completes; an exception that stops the build partway, such as a lost connection or a write concern the server could not satisfy, completes the future exceptionally instead. Builds on a connection run one at a time, so a build requested while another is running waits for it, whether either runs in the background or on the thread that called buildIndex(), and cancelling the future does not stop the build. If the datastore is stopped or closed, the future fails at once; a build that stopping the datastore cuts short fails its future and runs again, without one, when the datastore is restarted.

A declaration that fails does not fail startup, whatever the settings. To refuse to start without every declared index, wait for the build in BootStrap and stop on any failure. BootStrap runs before the application reports itself ready for traffic, and an exception thrown from it stops the application:

import org.grails.datastore.mapping.mongo.IndexBuildResult
import org.grails.datastore.mapping.mongo.MongoDatastore

class BootStrap {

    MongoDatastore mongoDatastore

    def init = { servletContext ->
        IndexBuildResult result = mongoDatastore.buildIndexAsync().get()
        if (result.failures()) {
            throw new IllegalStateException("${result.failures()} declared index(es) could not be built")
        }
    }
}

get() throws as well when an error stops the build. With buildIndexes left on, the startup build has already run by then, and this one confirms what it created and fails on the same declarations; set it to false to build only here.

Building Indexes in the Background

MongoDB answers a createIndex command only once the index has been built, so by default the thread that starts the datastore — in an application, the thread starting the application context — waits for every declared index before the application finishes starting. On an empty collection that is instant; on a large existing collection an index build can take minutes, and a deployment waits for all of them in turn.

Set buildIndexesAsync to have index builds run on a background thread instead, both the one GORM runs at startup and any the application starts with buildIndex():

grails:
    mongodb:
        buildIndexesAsync: true

Startup then continues without waiting. The indexes are still built one at a time per connection, on a daemon thread named after its connection, such as gorm-mongo-index-build-default-1. Different connections can build indexes concurrently, including when they use the same MongoDB server. The thread is released after the build finishes and it has been idle for one second.

Two consequences are worth planning for:

  • A query issued before its index has been built is served without it — correctly, but with the performance of an unindexed query. The same applies to a unique index: it constrains nothing until the build finishes.

  • Because startup no longer waits, an error that stops the build, such as a lost connection, can no longer fail startup. It is logged at error level, and the application runs without the indexes the build had not reached. With the default synchronous build that error propagates and the application does not start. A declaration the server refuses, such as a unique index over duplicate values, fails startup with neither setting: it is logged and counted, and the build goes on. To refuse to start without it, see Building Indexes on Demand.

The setting also applies with buildIndexes set to false, to the builds the application starts with buildIndex(). A domain class registered after startup is indexed on the thread registering it, once any build running on the connection has finished, and only when it is mapped to the default connection; if the datastore is stopped for a checkpoint at the time, it is indexed when the datastore is started again.

If the application shuts down while a background build is still running, GORM stops waiting for it, but the server carries on building the index it was asked for. A datastore stopped for a checkpoint and restored (with CRaC) runs a build it cut short again once it is restarted, on every connection, and logs that it is resuming it.

What the Index Build Reports

An index build that finishes without error logs one summary line at INFO:

Index build for database [myDb] finished in 412ms: 2 created, 5 already present, from 3 domain class(es)

The domain class count includes every class considered for indexing, including classes that declare no indexes. The created and already-present counts describe index declarations; declaring the same keys twice can report one created and one already present.

The elapsed time is what the caller actually spent waiting — startup, or whoever called buildIndex(), with the default settings, or the background thread when buildIndexesAsync is enabled, where this line is also the only signal that the build has finished.

The split between created and already present is what makes that time interpretable. MongoDB answers a createIndex for an index it already has immediately and without building anything, so a restart that changed no mappings reports everything as already present and costs milliseconds; a line reporting indexes created is the one that accounts for a slow start. An index that recreateOnConflict dropped and built again costs as much as a new one, and is counted apart as recreated:

Index build for database [myDb] finished in 9315ms: 0 created, 1 recreated, 6 already present, from 3 domain class(es)

A declaration the server refuses does not end the build: an option conflict GORM is not authorised to resolve, an invalid specification, or a unique index over documents that already share a value is logged at ERROR as it fails and counted, and the build goes on with the remaining declarations. Only an error that stops the build itself ends it early: a lost connection, or a write concern the server could not satisfy, which leaves unknown whether the index was built. With the default synchronous build that fails startup.

If any declaration failed, the summary is logged at WARN instead and reports how many; the failures themselves are logged individually as they happen. A build that stops partway — a lost connection or a timeout — reports how far it got before stopping, also at WARN:

Index build for database [myDb] did not finish, stopping after 812ms at 1 of 3 domain class(es): 1 created, 0 already present

Telling a created index from one that was already there takes one listIndexes command per collection that declares indexes. It is only issued when the summary will be logged, that is when INFO is enabled for org.grails.datastore.mapping.mongo.

A collection whose indexes cannot be listed still has its declarations applied; only the split between created and already present is missing for it. The summary counts those declarations separately, and the other collections keep their split:

Index build for database [myDb] finished in 412ms: 2 created, 5 already present, 3 applied without a listing, from 3 domain class(es)

When nothing could be classified — the logger is at WARN, so the build skips listIndexes and a summary is logged only for a build that failed or did not finish, or no collection’s indexes could be listed — the summary reports how many declarations were applied instead:

Index build for database [myDb] finished in 412ms: 10 index declaration(s) applied, 1 failed, from 3 domain class(es)

To find which index is the slow one, enable DEBUG logging for org.grails.datastore.mapping.mongo, which adds a line per index with its own elapsed time:

logging:
    level:
        org.grails.datastore.mapping.mongo: DEBUG

Finding Missing Indexes

With buildIndexes off, a declared index exists only once something builds it, and nothing reports one that has not been built yet. findMissingIndexes() on the MongoDatastore bean lists the indexes the domain classes declare that their collections do not have, without changing anything. Logged at startup, it shows when a build has been forgotten:

def missing = mongoDatastore.findMissingIndexes()
if (missing) {
    log.warn "Declared indexes not built: ${missing.collect { "${it.collection()} ${it.key().toJson()}" }}"
}

Each MissingIndex gives the database, the collection, the domain class declaring the index, its key pattern and the options declared with it. A declared index is present when its collection has an index on the same key pattern, the same fields in the same order, whatever its name and options: an option that differs is for the build to reconcile (see Reconciling Index Option Changes). A text index is present when one indexes the same text fields, in any order, with the same keys before and after them, whatever its weights. Keys that several classes mapped to one collection declare are reported once. buildIndex() or buildIndexAsync() creates what is missing. Each named connection covers its own domain classes, so call it through that connection’s datastore for those.

Finding and Dropping Undeclared Indexes

GORM never drops an index. One that a mapping stops declaring, or declares again with its keys in another order, stays on the server, still maintained on every write. findUndeclaredIndexes() on the MongoDatastore bean lists them without changing anything:

import org.grails.datastore.mapping.mongo.MongoDatastore
import org.grails.datastore.mapping.mongo.UndeclaredIndex

class IndexMaintenanceService {

    MongoDatastore mongoDatastore

    List<UndeclaredIndex> undeclaredIndexes() {
        mongoDatastore.findUndeclaredIndexes()
    }
}

Each UndeclaredIndex gives the database, collection, name and key pattern of the index, and the whole description listIndexes returned for it, options included.

An index counts as declared when a domain class mapped to its collection declares the same key pattern, the same fields in the same order, through compoundIndex, index or a property’s index: true. Its name and options do not matter: those differences are what the build reconciles (see Reconciling Index Option Changes). A text index counts as declared when a class declares text on the same fields, in any order, with the same keys before and after them, whatever its weights. The declarations of every class mapped to a collection count, so a subclass’s index on its root’s collection is declared. The _id index is never reported, and nor is any index on a collection that no domain class maps.

dropUndeclaredIndexes() drops everything findUndeclaredIndexes() reports, logging each index at INFO, and returns what it dropped. To drop only some, pass the reviewed list to dropUndeclaredIndexes(List). Each index is checked again before it is dropped, since the server can have changed since the list was made: an index or collection that has gone is skipped, and so, with a WARN, is an index whose name now belongs to an index on other keys, one whose keys a domain class now declares, and one on a collection that no class is mapped to on that connection:

def undeclared = mongoDatastore.findUndeclaredIndexes()
mongoDatastore.dropUndeclaredIndexes(undeclared.findAll { it.collection() != 'auditLog' })

Like the index build, each named connection covers its own domain classes, so call these through that connection’s datastore:

mongoDatastore.getDatastoreForConnection('reporting').findUndeclaredIndexes()
Drop indexes deliberately, once every instance of the application runs the release whose mappings declare the indexes to keep, and never automatically at startup. An instance still on an earlier release does not declare an index that a later release adds, or one created by hand ahead of a deployment, so to that instance those indexes are undeclared. An index that application code creates outside the mappings, by calling createIndex itself, is not a declaration either, and is reported and dropped like any other.

Indexing using the index method

In addition to the convenience features described above you can use the index method to define any index you want. For example:

static mapping = {
    index( [1],[person.address.postCode] [unique:true] )
}

In the above example I define an index on an embedded attribtue of the document. In fact what arguments you pass to the index method get passed to the underlying MongoDB createIndex method.

Customizing the WriteConcern

A feature of MongoDB is its ability to customize how important a database write is to the user. The Java client models this as a WriteConcern and there are various options that indicate whether the client cares about server or network errors, or whether the data has been successfully written or not.

If you wish to customize the WriteConcern for a domain class you can do so in the mapping block:

import com.mongodb.WriteConcern

class Person {
    String name
    static mapping = {
        writeConcern WriteConcern.FSYNC_SAFE
    }
}
For versioned entities, if a lower level of WriteConcern than WriteConcern.ACKNOWLEDGE is specified, WriteConcern.ACKNOWLEDGE will also be used for updates, to ensure that optimistic locking failures are reported.

Dynamic Attributes

Unlike a relational database, MongoDB allows for "schemaless" persistence where there are no limits to the number of attributes a particular document can have. A GORM domain class on the other hand has a schema in that there are a fixed number of properties. For example consider the following domain class:

class Plant {
    boolean goesInPatch
    String name
}

Here there are two fixed properties, name and goesInPatch, that will be persisted into the MongoDB document. Using GORM for MongoDB you can however use dynamic properties via the Groovy subscript operator. For example:

def p = new Plant(name:"Pineapple")
p['color'] = 'Yellow'
p['hasLeaves'] = true
p.save()

p = Plant.findByName("Pineapple")

println p['color']
println p['hasLeaves']

Using the subscript operator you can add additional attributes to the underlying Document instance that gets persisted to the MongoDB allowing for more dynamic domain models.

Custom User Types

GORM for MongoDB will persist all common known Java types like String, Integer, URL etc., however if you want to persist one of your own classes that is not a domain class you can implement a custom user type.

Custom Codecs

GORM for MongoDB is built ontop of MongoDB’s BSON encoding framework. This means it is possible to implement custom Codecs for encoding and decoding values to and from BSON.

For example consider the following simple Groovy class:

class Birthday {
    Date date
}

By default the encoding engine does not know how to represent this type as a BSON value. To make the encoding engine understand this type you have to implement a custom codec:

import org.bson.*
import org.bson.codecs.*

class BirthdayCodec implements Codec<Birthday> {
    Birthday decode(BsonReader reader, DecoderContext decoderContext) {
        return new Birthday(date: new Date(reader.readDateTime())) (1)
    }
    void encode(BsonWriter writer, Birthday value, EncoderContext encoderContext) {
        writer.writeDateTime(value.date.time) (2)
    }
    Class<Birthday> getEncoderClass() { Birthday } (3)
}
1 Decodes the Birthday type from the BsonReader
2 Encodes the Birthday type to the BsonWriter
3 Returns the type that is to be encoded. In this case Birthday.

With that done you then need to register the custom Codec. There are two ways to achieve this.

You can register a list of codecs in the grails.mongodb.codecs setting in application.yml:

grails:
    mongodb:
        codecs:
            - my.company.BirthdayCodec

Or you can create a META-INF/services/org.bson.codecs.Codec file containing the fully qualified class name of the Codec. If there are multiple codec classes you would like to register, put each one on a separate line.

Custom Types with GORM

Another option is to define a GORM custom type. For example consider the following class:

class Birthday implements Comparable{
    Date date

    Birthday(Date date) {
        this.date = date
    }

    @Override
    int compareTo(Object t) {
        date.compareTo(t.date)
    }
}
Custom types should go in src/groovy not grails-app/domain

If you attempt to reference this class from a domain class it will not automatically be persisted for you. However you can create a custom type implementation and register it with Spring. For example:

import groovy.transform.InheritConstructors
import org.bson.Document
import org.grails.datastore.mapping.engine.types.AbstractMappingAwareCustomTypeMarshaller
import org.grails.datastore.mapping.model.PersistentProperty
import org.grails.datastore.mapping.mongo.query.MongoQuery
import org.grails.datastore.mapping.query.Query

@InheritConstructors
class BirthdayType extends AbstractMappingAwareCustomTypeMarshaller<Birthday, Document, Document> {
   @Override
   protected Object writeInternal(PersistentProperty property, String key, Birthday value, Document nativeTarget) {
       final converted = value.date.time
       nativeTarget.put(key, converted)
       return converted
   }

   @Override
   protected void queryInternal(PersistentProperty property, String key, PropertyCriterion criterion, Document nativeQuery) {
       if (criterion instanceof Between) {
           def dbo = new BasicDBObject()
           dbo.put(MongoQuery.MONGO_GTE_OPERATOR, criterion.getFrom().date.time)
           dbo.put(MongoQuery.MONGO_LTE_OPERATOR, criterion.getTo().date.time)
           nativeQuery.put(key, dbo)
       }
       else {
           nativeQuery.put(key, criterion.value.date.time)
       }
   }

   @Override
   protected Birthday readInternal(PersistentProperty property, String key, Document nativeSource) {
       final num = nativeSource.get(key)
       if (num instanceof Long) {
           return new Birthday(new Date(num))
       }
       return null
   }
})

The above BirthdayType class is a custom user type implementation for MongoDB for the Birthday class. It provides implementations for three methods: readInternal, writeInternal and the optional queryInternal. If you do not implement queryInternal your custom type can be persisted but not queried.

The writeInternal method gets passed the property, the key to store it under, the value and the native DBObject where the custom type is to be stored:

@Override
protected Object writeInternal(PersistentProperty property, String key, Birthday value, DBObject nativeTarget) {
    final converted = value.date.time
    nativeTarget.put(key, converted)
    return converted
}

You can then read the values of the custom type and register them with the DBObject. The readInternal method gets passed the PersistentProperty, the key the user type info is stored under (although you may want to use multiple keys) and the DBObject:

@Override
protected Birthday readInternal(PersistentProperty property, String key, Document nativeSource) {
    final num = nativeSource.get(key)
    if(num instanceof Long) {
        return new Birthday(new Date(num))
    }
    return null
}

You can then construct the custom type by reading values from the DBObject. Finally the queryInternal method allows you to handle how a custom type is queried:

@Override
protected void queryInternal(PersistentProperty property, String key, Query.PropertyCriterion criterion, Document nativeQuery) {
    if(criterion instanceof Between) {
        def dbo = new BasicDBObject()
        dbo.put(MongoQuery.MONGO_GTE_OPERATOR, criterion.getFrom().date.time);
        dbo.put(MongoQuery.MONGO_LTE_OPERATOR, criterion.getTo().date.time);
        nativeQuery.put(key, dbo)
    }
    else if(criterion instanceof Equals){
        nativeQuery.put(key, criterion.value.date.time)
    }
    else {
            throw new RuntimeException("unsupported query type for property $property")
    }
}

The method gets passed a criterion which is the type of query and depending on the type of query you may handle the query differently. For example the above implementation supports between and equals style queries. So the following 2 queries will work:

Person.findByBirthday(new Birthday(new Date()-7)) // find someone who was born 7 days ago
Person.findByBirthdayBetween(new Birthday(new Date()-7), new Birthday(new Date())) // find someone who was born in the last 7 days

However "like" or other query types will not work.

To register a custom type in a grails application simply register it as Spring bean. For example, to register the above BirthdayType add the following to grails-app/conf/spring/resources.groovy:

import com.example.*

// Place your Spring DSL code here
beans = {
  birthdayType(BirthdayType, Birthday)
}

Querying

Basic Querying

GORM for MongoDB supports all of the regular methods for executing GORM queries apart from HQL, which is a Hibernate specific query language more appropriate for SQL databases.

If you wish to execute a native MongoDB query you can use the find method that takes a Bson argument. For example:

import static com.mongodb.client.model.Filters.eq

...
FindIterable findIterable = Product.find(eq("title", "coffee"))
findIterable.limit(10)
        .each { Product product ->
            println "Product title $product.title"
        }

The find method will return a FindIterable instance that you can then use to further customize via filters, sorting and projections.

For the full MongoDB client model refer to the com.mongodb.client.model package.

The find method will return instances of your domain class for each query. If you wish to instead obtain MongoDB Document instance then you should use the collection property of the domain class:

import static com.mongodb.client.model.Filters.eq

...
Document doc = Product.collection
        .find(eq("title", "coffee"))
        .first()

Criterion values

A Map passed as a criterion value to findWhere, findAllWhere, a criteria query, the where DSL or a dynamic finder is sent to MongoDB as a literal subdocument comparison. Because MongoDB interprets a value document whose keys start with $ as an operator expression rather than as a value, a map that contains such a key at any depth is rejected with InvalidDataAccessResourceUsageException. This applies to every property type, including enums and other custom types; geospatial criteria take shape documents by design and are exempt.

If you need to run an operator query that GORM does not express, use the native find method with a Bson filter as shown above.

Bulk Updates

updateAll sets the given properties on every document the criteria match, in a single updateMany, without loading the entities:

Ticket.where { status == 'open' }.updateAll(assignee: user)

An association value is written the way normal persistence writes it: the target’s identifier, in the type that target’s _id is stored as, and as a DBRef where the mapping declares reference: true. Either the associated instance or its identifier can be passed, so updateAll(assignee: user) and updateAll(assignee: user.id) write the same value.

Whether an association can be bulk-updated depends on which document holds the reference:

Association Supported Reason

to-one reference

Yes

The reference is a field on this document.

unidirectional one-to-many, many-to-many

Yes

The identifiers are stored as an array on this document.

embedded, and collections of simple values such as hasMany = [labels: String]

Yes

The value is written like any other property.

hasOne

No

The foreign key is held by the associated document.

bidirectional one-to-many

No

The foreign key is held by each child.

The unsupported kinds throw UnsupportedOperationException rather than writing a field normal persistence never reads. Update the side that holds the foreign key instead. For a bidirectional one-to-many, that means updating the children:

Child.where { id in childIds }.updateAll(parent: newParent)

Geospacial Querying

MongoDB supports storing Geospacial data in both flat and spherical surface types.

To store data in a flat surface you use a "2d" index, whilst a "2dsphere" index used for spherical data. GORM for MongoDB supports both and the following sections describe how to define and query Geospacial data.

Geospacial 2D Sphere Support

Using a 2dsphere Index

MongoDB’s 2dsphere indexes support queries that calculate geometries on an earth-like sphere.

Although you can use coordinate pairs in a 2dsphere index, they are considered legacy by the MongoDB documentation and it is recommended you store data using GeoJSON Point types.

MongoDB legacy coordinate pairs are in latitude / longitude order, whilst GeoJSON points are stored in longitude / latitude order!

To support this GORM for MongoDB features a special type, grails.mongodb.geo.Point, that can be used within domain classes to store geospacial data:

import grails.mongodb.geo.*
...
class Restaurant {
    ObjectId id
    Point location

    static mapping = {
        location geoIndex:'2dsphere'
    }
}

The Point type gets persisted as a GeoJSON Point. A Point can be constructed from coordinates represented in longitude and latitude (the inverse of 2d index location coordinates!). Example:

Restaurant r = new Restaurant(location: new Point(50, 50))
r.id = "Dan's Burgers"
r.save(flush:true)

Restaurant.findByLocation(new Point(50,50))

Querying a 2dsphere Index

Once the 2dsphere index is in place you can use various MongoDB plugin specific dynamic finders to query, including:

  • findBy…​GeoWithin - Find out whether a Point is within a Box, Polygon, Circle or Sphere

  • findBy…​GeoIntersects - Find out whether a Point is within a Box, Polygon, Circle or Sphere

  • findBy…​Near - Find out whether any GeoJSON Shape is near the given Point

  • findBy…​NearSphere - Find out whether any GeoJSON Shape is near the given Point using spherical geometry.

Some examples:

Restaurant.findByLocationGeoWithin( Polygon.valueOf([ [0, 0], [100, 0], [100, 100], [0, 100], [0, 0] ]) )
Restaurant.findByLocationGeoWithin( Box.valueOf( [[25, 25], [100, 100]] ) )
Restaurant.findByLocationGeoWithin( Circle.valueOf( [[50, 50], 100] ) )
Restaurant.findByLocationGeoWithin( Sphere.valueOf( [[50, 50], 0.06]) )
Restaurant.findByLocationNear( Point.valueOf( 40, 40 ) )
Note that a Sphere differs from a Circle in that the radius is specified in radians. There is a special Distance class that can help with radian calculation.

Native Querying Support

In addition to being able to pass any Shape to geospacial query methods you can also pass a map that represents the native values to be passe to the underlying query. For example:

def results = Restaurant.findAllByLocationNear( [$geometry: [type:'Point', coordinates: [1,7]], $maxDistance:30000] )

In the above example the native query parameters are simply passed to the $near query

Geospacial 2D Index Support

MongoDB supports 2d indexes that store points on a two-dimensional plane. although they are considered legacy and you should use 2dsphere indexes instead.

It is possible to use a MongoDB 2d index by mapping a list or map property using the geoIndex mapping:

class Hotel {
    String name
    List location

    static mapping = {
        location geoIndex:'2d'
    }
}

By default the index creation assumes latitude/longitude and thus is configured for a -180..180 range. If you are indexing something else you can customise this with indexAttributes

class Hotel {
    String name
    List location

    static mapping = {
        location geoIndex:'2d', indexAttributes:[min:-500, max:500]
    }
}

You can then save Geo locations using a two dimensional list:

new Hotel(name:"Hilton", location:[50, 50]).save()

Alternatively you can use a map with keys representing latitude and longitude:

new Hotel(name:"Hilton", location:[lat: 40.739037d, long: 73.992964d]).save()
You must specify whether the number of a floating point or double by adding a d or f at the end of the number eg. 40.739037d. Groovy’s default type for decimal numbers is BigDecimal which is not supported by MongoDB.

Once you have your data indexed you can use MongoDB specific dynamic finders to find hotels near a given a location:

def h = Hotel.findByLocationNear([50, 60])
assert h.name == 'Hilton'

You can also find a location within a box (bound queries). Boxes are defined by specifying the lower-left and upper-right corners:

def box = [[40.73083d, -73.99756d], [40.741404d,  -73.988135d]]
def h = Hotel.findByLocationWithinBox(box)

You can also find a location within a circle. Circles are specified using a center and radius:

def center = [50, 50]
def radius = 10
def h = Hotel.findByLocationWithinCircle([center, radius])

If you plan on querying a location and some other value it is recommended to use a compound index:

class Hotel {
    String name
    List location
    int stars

    static mapping = {
        compoundIndex location:"2d", stars:1
    }
}

In the example above you an index is created for both the location and the number of stars a Hotel has.

GeoJSON Data Models

You can also store any GeoJSON shape using the grails.mongodb.geo.Shape super class:

import grails.mongodb.geo.*
...
class Entry {
    ObjectId id
    Shape shape

    static mapping = {
        shape geoIndex:'2dsphere'
    }
}
...
new Entry(shape: Polygon.valueOf([[[3, 1], [1, 2], [5, 6], [9, 2], [4, 3], [3, 1]]]) ).save()
new Entry(shape: LineString.valueOf([[5, 2], [7, 3], [7, 5], [9, 4]]) ).save()
new Entry(shape: Point.valueOf([5, 2])).save()

And then use the findBy*GeoIntersects method to figure out whether shapes intersect with each other:

assert Entry.findByShapeGeoIntersects( Polygon.valueOf( [[ [0,0], [3,0], [3,3], [0,3], [0,0] ]] ) )
assert Entry.findByShapeGeoIntersects( LineString.valueOf( [[1,4], [8,4]] ) )

Full Text Search

Using MongoDB 2.6 and above you can create full text search indices.

To create a "text" index using the index method inside the mapping block:

class Product {
    ObjectId id
    String title

    static mapping = {
        index title:"text"
    }
}

You can then search for instances using the search method:

assert Product.search("bake coffee cake").size() == 10
assert Product.search("bake coffee -cake").size() == 6

You can search for the top results by rank using the searchTop method:

assert Product.searchTop("cake").size() == 4
assert Product.searchTop("cake",3).size() == 3

And count the number of hits with the countHits method:

assert Product.countHits('coffee') == 5

Multiple Data Sources

GORM for MongoDB supports the notion of multiple data sources where multiple individual MongoClient instances can be configured and switched between.

Configuring Multiple Mongo Clients

To configure multiple Mongo client connections you need to use the grails.mongodb.connections setting. For example in application.yml:

grails-app/conf/application.yml
grails:
    mongodb:
        url: mongodb://localhost/books
        connections:
            moreBooks:
                url: mongodb://localhost/moreBooks
            evenMoreBooks:
                url: mongodb://localhost/moreBooks

You can configure individual settings for each Mongo client. If a setting is not specified by default the setting is inherited from the default Mongo client.

Mapping Domain Classes to Mongo Clients

If a domain class has no specify Mongo client connection configuration then the default is used.

You can set the connection method in the mapping block to configure an alternate Mongo Client.

For example, if you want to use the ZipCode domain to use a Mongo client connection called 'lookup', configure it like this:

class ZipCode {

   String code

   static mapping = {
      connection 'lookup'
   }
}

A domain class can also use two or more configured Mongo client connections by using the connections method with a list of names to configure more than one, for example:

class ZipCode {

   String code

   static mapping = {
      connections(['lookup', 'auditing'])
   }
}

If a domain class uses the default connection and one or more others, you can use the ConnectionSource.DEFAULT constant to indicate that:

import org.grails.datastore.mapping.core.connections.*

class ZipCode {

   String code

   static mapping = {
      connections(['lookup', ConnectionSource.DEFAULT])
   }
}

If a domain class uses all configured DataSource instances use the value ALL:

import org.grails.datastore.mapping.core.connections.*

class ZipCode {

   String code

   static mapping = {
      connection ConnectionSource.ALL
   }
}

Switching between Mongo Clients

You can switch to a different connection at runtime with the withConnection method:

Book.withConnection("moreBooks") {
    Book.list()
}

Every operation on Book in the closure, on the thread that calls withConnection, uses the alternate connection: static methods called on the class, such as Book.list() or a dynamic finder, instance methods such as book.save(), and the methods the closure calls without naming the class. An operation that names its own connection, such as Book.moreBooks.count(), keeps it. For a multi-tenant domain class the block’s connection takes precedence over the current tenant, as a connection named explicitly does. Once the closure finishes, including by throwing an exception, GORM switches back to the default connection automatically.

A session or transaction opened through a connection covers its block in the same way. Inside Book.moreBooks.withTransaction { } or Book.moreBooks.withNewSession { }, Book.list() and book.save() use moreBooks, whose session and transaction are the ones open. Other domain classes are not affected. A method annotated @Transactional(connection = 'moreBooks') routes in the same way, for every domain class that declares moreBooks in its connections mapping, ALL included, and leaves a multi-tenant class to its tenant. A class that declares no connection is reachable through Book.moreBooks but is not routed by the annotation.

The ConnectionSources API

Introduced in GORM 6.0, the ConnectionSources API allows you to introspect the data sources configured for the application:

@Autowired
MongoDatastore mongoDatastore
...
ConnectionSources<MongoClient, MongoConnectionSourceSettings> connectionSources
                                        = mongoDatastore.getConnectionSources()

for(ConnectionSource<MongoClient, MongoConnectionSourceSettings> connectionSource in connectionSources) {
        println "Name $connectionSource.name"
        MongoClient mongoClient = connectionSource.source
}

Switching Database or Collection at Runtime

In addition to storing dynamic attributes, as of version 1.3.0 of the plugin you can also switch which database and/or collection to persist to at runtime.

For example:

Person.withDatabase("administrators") {
    new Person(name:"Bob").save()
}

The above example will save a Person instance to the administrators database. The database is used for the scope of the closure. You can switch database for the scope of the active session:

Person.useDatabase("administrators")
new Person(name:"Bob").save()

In addition, there are equivalent withCollection and useCollection methods for switching collection at runtime.

Multi-Tenancy

GORM for MongoDb supports the following multi-tenancy modes:

  • DATABASE - A separate database with a separate connection pool is used to store each tenants data.

  • SCHEMA - The same database, but different schemas are used to store each tenants data.

  • DISCRIMINATOR - The same database is used with a discriminator used to partition and isolate data.

Configuring Multi Tenancy

You can configure Multi-Tenancy the same way described in the GORM for Hibernate documenation, simply specify a multi tenancy mode and resolver:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver

Note that if you are using MongoDB and Hibernate together the above configuration will configure both MongoDB and Hibernate to use a multi-tenancy mode of DATABASE.

If you only want to enable multi-tenancy for MongoDB only you can use the following configuration instead:

grails:
    mongodb:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver

Multi-Tenancy Transformations

The following transformations can be applied to any class to simplify greatly the development of Multi-Tenant applications. These include:

  • @CurrentTenant - Resolve the current tenant for the context of a class or method

  • @Tenant - Use a specific tenant for the context of a class or method

  • @WithoutTenant - Execute logic without a specific tenant (using the default connection)

For example:

import grails.gorm.multitenancy.*

// resolve the current tenant for every method
@CurrentTenant
class TeamService {

    // execute the countPlayers method without a tenant id
    @WithoutTenant
    int countPlayers() {
        Player.count()
    }

    // use the tenant id "another" for all GORM logic within the method
    @Tenant({"another"})
    List<Team> allTwoTeams() {
        Team.list()
    }

    List<Team> listTeams() {
        Team.list(max:10)
    }

    @Transactional
    void addTeam(String name) {
        new Team(name:name).save(flush:true)
    }
}

Multi Tenancy Modes

As mentioned previously, GORM for MongoDB supports all three multi tenancy modes however there are some considerations to keep in mind.

Database Per Tenant

When using the DATABASE mode, only GORM methods calls are dispatched to the correct tenant. This means the following will use the tenant id:

// switches to the correct client based on the tenant id
Book.list()

However, going directly through the MongoClient will not work:

@Autowired MongoClient mongoClient

// uses the default connection and doesn't resolve the tenant it
mongoClient.getDatabase("book").find()

If you are working directly with the MongoClient instance you need to make sure you obtain the correct instance. For example:

import grails.gorm.multitenancy.*

@Autowired MongoDatastore mongoDatastore
...
MongoClient mongoClient =
        mongoDatastore.getDatastoreForTenantId(Tenants.currentId())
                      .getMongoClient()

Schema Per Tenant

When using the SCHEMA mode, GORM for MongoDB will use a different MongoDB database, but the same MongoClient instance, for each tenant.

However, once again only GORM methods will use the correct database. For example:

// switches to the correct database based on the tenant id
Book.list()

However, getting the database directly from MongoClient will not work:

@Autowired MongoClient mongoClient

// uses the default connection and doesn't resolve the tenant it
mongoClient.getDatabase("book").find()

To resolve this you should always use the DB property of the class which will ensure the right database is used:

// switches to the correct database based on the tenant id
Book.DB.find()

Partitioned Multi-Tenancy

When using the DISCRIMINATOR approach, GORM for MongoDB will store a tenantId attribute in each MongoDB document and attempt to partition the data.

Once again this works only when using GORM methods and even then there are cases where it will not work if you use native MongoDB interfaces.

For example the following works fine:

// correctly includes the `tenantId` in the query
Book.list()

As does this:

import static com.mongodb.client.model.Filters.*;

// correctly includes the `tenantId` in the query
Book.find(eq("title", "The Stand")).first()

But this logic bypasses any built into tenant id interception and inclusion:

Book.collection.find().first()

Since you are operating directly on the collection GORM cannot know when you perform a query on said collection.

In this case you will have to ensure to include the tenantId manually:

import static com.mongodb.client.model.Filters.*;
...
Book.collection.find(eq("tenantId", Tenants.currentId())).first()

And the same is true of write operations such as inserts that are done with the native API.

Dynamic ConnectionSources

If you are using a multi-tenancy mode of DATABASE then by default the expectation is that all tenants are configured in your application.yml file.

However, it is possible read your Mongo client connection sources dynamically using MongoConnectionSources.

The MongoConnectionSources class will read the mongo client configurations from a Mongo collection called mongo.connections by default. To configure it you must specify the connectionSourcesClass in application.yml:

grails:
    mongodb:
        multiTenancy:
            mode: DATABASE
        connectionSourcesClass: org.grails.datastore.mapping.mongo.connections.MongoConnectionSources
        connectionsCollection: "myconnections"
...

You can then even add new connections at runtime using the ConnectionSources API:

import grails.gorm.multitenancy.*

@Autowired MongoDatastore mongoDatastore
...
def configuration = [url:"mongodb://localhost/moreBooks"]
MongoClient mongoClient =
        mongoDatastore.connectionSources
                                          .addConnectionSource("moreBooks", configuration)

All new connection sources will be stored within the specified connectionsCollection and if the application is restarted will read from the connectionsCollection.

GORM for MongoDB does not implement provisioning of new MongoDB instances at runtime. This is something that would need to be implemented by a cloud services provider for example.

Stateless Mode

GORM for MongoDB supports both stateless and stateful modes for mapping domain classes to MongoDB. In general stateful mapping is superior for write heavy applications and stateless mode better for read heavy applications (particularily when large amounts of data is involved).

Stateful mode

Domain classes are by default stateful, which means when they are read from a MongoDB document their state is stored in the user session (which is typically bound to the request in Grails). This has several advantages for write heavy applications:

  • GORM can automatically detect whether a call to save() is an update or an insert and act appropriately

  • GORM stores the state of the read MongoDB document and therefore updates to schemaless properties don’t require an extra query

  • GORM can store the current version and therefore implement optimistic locking

  • Repeated reads of the same entity can be retrieved from the cache, thus optimizing reads as well

For an example of when a stateful domain class is better consider the following:

def b = Book.get(1)
b['pages'] = 400
b['publisher'] = 'Manning'
b['rating'] = 5
b.save(flush:true)

With a stateful entity the updates to the three properties can be batched up and executed in the save() call, when there is no state then 3 updates needs to be executed for each schemaless property (ouch!).

Stateless Domain classes

However, stateful domain classes can cause problems for read-heavy applications. Take for example the following code:

def books = Book.list() // read 100,000 books
for(b in books) {
    println b.title
}

The above example will read 100,000 books and print the title of each. In stateful mode this will almost certainly run out of memory as each MongoDB document is stored in user memory as is each book. Rewriting the code as follows will solve the problem:

Book.withStatelessSession {
    def books = Book.list() // read 100,000 books
    for(b in books) {
        println b.title
    }
}

Alternatively you can map the domain class as stateless, in which case its state will never be stored in the session:

class Book {
    ...
    static mapping = {
        stateless true
    }
}

Disadvantages of Stateless Mode

There are several disadvantages to using stateless domain classes as the default. One disadvantage is that if you are using assigned identifiers GORM cannot detect whether you want to do an insert or an update so you have to be explicit about which one you want:

Book b = new Book()
b.id = "The Book"
b.insert()

In the above case we use the explicit insert method to tell Grails this is an insert not an udpate. Another disadvantage is that reading of schemaless/dynamic properties is more costly. For example:

def books = Book.list() // read 100,000 books
for(b in books) {
    println b['pages']
    println b['rating']
}

Here GORM has to execute an additional read method for each schemaless property! This is better written as:

def books = Book.list() // read 100,000 books
for(b in books) {
    def dbo = b.dbo
    println dbo['pages']
    println dbo['rating']
}

Thus only requiring one query. Or alternatively you can use the native API:

def books = Book.collection.find() // read 100,000 books
for(dbo in books) {
    Book b = dbo as Book
    println dbo['pages']
    println dbo['rating']
}

Which would be more efficient.

Using the MongoDB Driver Directly

A lower level API is provided by the plugin via the MongoDB driver

There is an excellent tutorial on how to use the MongoDB Java driver’s API directly in the MongoDB documentation

An example can be seen below:

// Get a db reference in the old fashion way
def db = mongo.getDatabase("mydb")

// Insert a document
db.languages.insert([name: 'Groovy'])
// A less verbose way to do it
db.languages.insert(name: 'Ruby')
// Yet another way
db.languages << [name: 'Python']

// Insert a list of documents
db.languages << [[name: 'Javascript', type: 'prototyped'], [name: 'Ioke', type: 'prototyped']]

To get hold of the mongo instance (which is an instance of the com.mongodb.Mongo class) inside a controller or service simple define a mongo property:

def mongo
def myAction = {
    def db = mongo.getDatabase("mongo")
    db.languages.insert([name: 'Groovy'])
}

A request scoped bean is also available for the default database (typically the name of your application, unless specified by the databaseName config option, plus the suffix "DB").

def peopleDB
def myAction = {
    peopleDB.languages.insert([name: 'Fred'])
}

Each domain class you define also has a collection property that allows easy access to the underlying Collection instance:

Person.collection.count() == 1
Person.collection.findOne(firstName:"Fred").lastName == "Flintstone"

You can easily convert from a native MongoDB Document into an entity using a cast:

def fred = Person.collection.findOne(firstName:"Fred") as Person

Transactions

By default GORM for MongoDB does not use server-side transactions, however it does batch up inserts and updates until the session is flushed. This makes it possible to support some rollback options. To run transactions as real MongoDB multi-document transactions, see Multi-Document Transactions.

You can use either transactional services or the static withTransaction method. To mark a service as using the MongoDB transaction manager, use the static transactional property with the value 'mongo':

static transactional = 'mongo'

Alternately you can do ad-hoc transactions using the withTransaction method:

Person.withTransaction { status ->
    new Person(name:"Bob", age:50).save()
    throw new RuntimeException("bad")
    new Person(name:"Fred", age:45).save()
}

For example in this case neither Person object will be persisted to the database, because underneath the surface a persistence session is being used to batch up both insert operations into a single insert. When an exception is thrown neither insert is ever executed, hence we allow for some transactional semantics at the GORM-level.

Using the lower level API you can of course also take advantage of Mongo’s support for Atomic operations.

Nested Transactions

A transaction started while another is in progress joins it. This is the default propagation, PROPAGATION_REQUIRED, and it covers a transactional service method called from another, withTransaction inside withTransaction, and a @ReadOnly method called from a read-write one:

class OrderService {
    InventoryService inventoryService

    @Transactional
    void placeOrder(Order order) {
        order.save()
        inventoryService.reserve(order)   // @Transactional: joins placeOrder's transaction
        new AuditEntry(order: order).save()
    }
}

All three writes are committed together when placeOrder returns, and with multi-document transactions enabled they run in one server-side transaction. A joined transaction that fails rolls back the whole transaction, including writes made before it: when the exception propagates out of placeOrder, and also when placeOrder catches it, because GORM marks the surrounding transaction rollback-only. Spring’s TransactionTemplate, used directly, reports that case by throwing UnexpectedRollbackException from the outer commit. Without multi-document transactions, "rolls back" means that writes still queued in the session are discarded; a write already flushed stays written.

Without multi-document transactions, GORM for MongoDB does not flush the session before running a query within a transaction, so that a rollback can still discard what is queued. A write the transaction has queued, including one a joined transaction made, is therefore not visible to its later queries until it is flushed. Save with flush: true where a later query in the same transaction must see the write.

With multi-document transactions enabled, a flushed write is rolled back with the server-side transaction, so GORM flushes the session before a query, as it does outside a transaction and as Hibernate does. A query sees the writes the transaction has queued, including those of a joined transaction. A read-only transaction has no server-side transaction and keeps the session in COMMIT flush mode, so its queries still do not flush.

A session opened with withNewSession inside a transaction is separate from it: a transaction begun in that session is its own, and commits when it returns. Transactional code run from a transaction’s afterCommit or afterCompletion callbacks, such as an @TransactionalEventListener method, likewise begins a transaction of its own, since the one that triggered it can no longer commit.

PROPAGATION_REQUIRES_NEW suspends the surrounding transaction and runs in a session of its own, and, with multi-document transactions enabled, in a server-side transaction of its own. It commits or rolls back independently, and the surrounding transaction resumes afterwards:

@Transactional(propagation = Propagation.REQUIRES_NEW)
void recordAttempt(Order order) {
    new Attempt(order: order).save()   // committed even if the caller's transaction rolls back
}

The new session starts empty. An entity loaded in the surrounding transaction belongs to that transaction’s session, so load it again inside the REQUIRES_NEW transaction rather than saving the instance you were given, and settings made on the surrounding session, such as the database or collection chosen with withDatabase or withCollection, do not carry over.

With multi-document transactions enabled, a REQUIRES_NEW transaction is open beside its suspended caller, and MongoDB refuses a write that conflicts with another open transaction (WriteConflict): a document the caller has already written in its transaction, or a collection both create by writing to it. Create such collections up front, and keep the two transactions to different documents.

A read-write transaction started inside a read-only one joins it and is read-only, as it is on Hibernate: the read-only commit does not flush, so a write it left queued is not saved, and the commit logs the warning described under Read-Only Transactions. Call such a method from read-write code, or give it PROPAGATION_REQUIRES_NEW so it commits in a transaction of its own. A write it flushes explicitly with save(flush: true) is saved, but it is not part of any transaction: a read-only transaction has no server-side transaction, even with multi-document transactions enabled, so a later failure in the read-only code cannot roll it back. PROPAGATION_NESTED is not supported.

In earlier versions a transaction started inside another began a second transaction on the same session instead of joining the first. It committed on its own when it returned, and the surrounding transaction’s commit then did nothing, so writes the surrounding transaction made after the call were never saved; with multi-document transactions enabled, beginning the second transaction also discarded the writes made before the call.

Read-Only Transactions

A read-only transaction, such as a service method annotated with @ReadOnly or @Transactional(readOnly = true), commits without flushing the session. This holds whether or not multi-document transactions are enabled, and is how read-only transactions behave on GORM’s other datastores — on Hibernate, for instance, a read-only transaction puts the session into MANUAL flush mode.

It matters for a write that is saved without flush: true and is still queued when read-only code runs in the same session:

class ReportService {
    @ReadOnly
    int countPeople() {
        Person.count()
    }
}

new Person(name: "Fred").save()   // queued, not yet flushed
reportService.countPeople()       // read-only: commits without flushing

The read-only transaction does not persist Fred. While it runs the session is in COMMIT flush mode, so its queries do not flush the queued insert either. Once it completes the session’s flush mode is put back, and Fred stays queued until something flushes the session: a read-write transaction’s commit, save(flush: true), or the flush at the end of the request. If nothing does, the insert is discarded when the session closes.

In earlier versions, committing a read-only transaction flushed the session, so a write like this was persisted as a side effect of the read. If your code relies on that, flush the write explicitly with save(flush: true), or perform it inside a read-write transaction.

To help find such code, a read-only transaction that commits while its session still holds queued inserts, updates or deletes logs a warning from org.grails.datastore.mapping.transactions.DatastoreTransactionManager. The warning names the session and reports that the transaction did not persist the queued operations; it does not say whether a later read-write transaction in the same session will.

With multi-document transactions enabled, a read-only transaction reads without a server-side transaction. Its reads are not held to the server’s transactionLifetimeLimitSeconds, they honour the client’s read preference (inside a transaction the driver refuses any preference but primary), and they are not taken from one snapshot. A read-only transaction that joins a read-write one reads within that transaction.

When GORM and Spring Data MongoDB share a transaction, readOnly only governs GORM’s flush. See Spring Data MongoDB Interoperability.

Unit Testing

To write unit tests with MongoDB and Spock you can simply extend from grails.test.mongodb.MongoSpec.

MongoSpec is an abstract class that will initialise GORM in the setup phase of the specification being executed. It uses by default a MongoClient instance that connects to a MongoDB instance as defined in your configuration (by default, 127.0.0.1 and port 27017, see Getting Started for more details):

It is preferable to use testcontainers to automatically run MongoDB in a containerized environment and not have to run a MongoDB instance locally. The following examples use testcontainers:

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package functional.tests

import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import groovy.transform.CompileStatic
import org.testcontainers.containers.MongoDBContainer

@CompileStatic
trait EmbeddedMongoClient {

    abstract MongoDBContainer getMongoDBContainer()

    MongoClient createMongoClient() {
        if (!mongoDBContainer.isRunning()) {
            mongoDBContainer.start()
        }
        return MongoClients.create(mongoDBContainer.getReplicaSetUrl())
    }
}
import grails.test.mongodb.MongoSpec
import grails.validation.ValidationException
import org.apache.grails.testing.mongo.AbstractMongoGrailsExtension
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName
import spock.lang.AutoCleanup
import spock.lang.Shared

class LocalMongoUnitSpec extends MongoSpec implements EmbeddedMongoClient {

    @Shared
    @AutoCleanup
    final MongoDBContainer mongoDBContainer = new MongoDBContainer(AbstractMongoGrailsExtension.desiredMongoDockerName)

    void "test fail on error"() {

        when:
        def invalid = new Book(title: "")
        invalid.save()

        then:
        thrown ValidationException
        invalid.hasErrors()
    }
}

You can also use your own low-level MongoClient instance, as shown in the following example:

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

package functional.tests

import grails.test.mongodb.MongoSpec
import org.apache.grails.testing.mongo.AbstractMongoGrailsExtension
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName
import spock.lang.AutoCleanup
import spock.lang.Shared

class BookUnitSpec extends MongoSpec implements EmbeddedMongoClient {

    @Shared
    @AutoCleanup
    final MongoDBContainer mongoDBContainer = new MongoDBContainer(AbstractMongoGrailsExtension.desiredMongoDockerName)

    void "Test low-level API extensions"() {
        when:
        def db = createMongoClient().getDatabase("test")
//        db.drop()
        // Insert a document
        db['languages'].insert([name: 'Groovy'])
        // A less verbose way to do it
        db.languages.insert(name: 'Ruby')
        // Yet another way
        db.languages << [name: 'Python']

        then:
        db.languages.count() == 3
    }

    void "Test GORM access"(){
        when:
        Book book = new Book(title: 'El Quijote').save(flush: true)

        then:
        Book.count() ==1

        when:
        book = Book.findByTitle('El Quijote')

        then:
        book.id
    }

}

Note that the default implementation is to scan your classpath searching for domain classes, from the package defined in the configuration property grails.codegen.defaultPackage, and all the way down its subpackages. If your application is large, classpath scanning may be slow, so it’s better to override the method getDomainClasses():

@Override
protected List<Class> getDomainClasses() {
    [Book]
}

Integration Testing

There is a plugin available that will execute an in memory Mongo database during your integration tests. Data will be cleared between test cases so they can work similarly to H2 with @Rollback.

Visit the github page of the Embedded MongoDB Grails Plugin to learn more.

Reference

Beans

mongo

Purpose

Provides access to the native MongoClient instance.

Examples
MongoClient mongo

class FooController {
    MongoClient mongo
    def myAction() {
        MongoDatabase db = mongo.getDatabase("mongo")
        db.languages.insert([name: 'Groovy'])
    }
}
Description

See the API for the Mongo Java Driver for API usage info.

Domain Classes

collection

Purpose

Returns the MongoDB collection used for the current domain class

Examples
def bookBson = Book.collection.find().first()
Description

The collection property allows access to the underlying MongoDB MongoCollection object, thus allowing direct access to the low-level MongoDB driver.

collectionName

Purpose

Returns the name of the MongoDB collection used for the current domain class

Examples
println Book.collectionName
Description

The collectionName property allows introspection of the name of the DBCollection object used by a given domain class. Can be used in conjunction with useCollection to switch to different collections and back again.

countHits

Purpose

Executes a MongoDB $text search query and returns the number of hits.

Examples
assert Product.countHits("coffee") == 5
Description

The countHits method uses MongoDB’s full text search support to perform full text search on a "text" index and return the size of the returned cursor.

DB

Purpose

Returns the MongoDB MongoDatabase object.

Examples
MongoCollection dbCollection = Book.DB.getCollection("books")
Description

The DB property allows access to the underlying MongoDB MongoDatabase object, thus allowing easy access to the low-level MongoDB Java driver.

dbo

Purpose

Returns the MongoDB Document for an instance of a domain class

Using the Document object directly is discouraged, because it’s inefficient. It’s better to use Dynamic Attributes.
Examples
def b = Book.get(1)

println b.dbo
Description

The dbo property allows access to the underlying MongoDB Document, which is a respresentation of the stored BSON document that can be manipulated in memory.

findByGeoIntersects

Purpose

Executes a MongoDB $geoIntersects query

Examples

Given:

import grails.mongodb.geo.*
...
class Entry {
    ObjectId id
    Shape shape

    static mapping = {
        shape geoIndex:'2dsphere'
    }
}
...
new Entry(shape: Polygon.valueOf([[[3, 1], [1, 2], [5, 6], [9, 2], [4, 3], [3, 1]]]) ).save()
new Entry(shape: LineString.valueOf([[5, 2], [7, 3], [7, 5], [9, 4]]) ).save()
new Entry(shape: Point.valueOf([5, 2])).save()

And then use the findBy*GeoIntersects method to figure out whether shapes intersect with each other:

assert Entry.findByShapeGeoIntersects( Polygon.valueOf( [[ [0,0], [3,0], [3,3], [0,3], [0,0] ]] ) )
assert Entry.findByShapeGeoIntersects( LineString.valueOf( [[1,4], [8,4]] ) )
// native query
assert Entry.findByShapeGeoIntersects( [ $geometry : [type: "Polygon" ,
                                                      coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ] ]
                                                      ]
                                       ])
Description

The $geoIntersects operator is a geospatial query operator that selects all locations that intersect with a GeoJSON object. See $geoIntersects.

findByGeoWithin

Purpose

Executes a MongoDB $geoWithin query

Examples
Restaurant.findByLocationGeoWithin( Polygon.valueOf([ [0, 0], [100, 0], [100, 100], [0, 100], [0, 0] ]) )
Restaurant.findByLocationGeoWithin( Box.valueOf( [[25, 25], [100, 100]] ) )
Restaurant.findByLocationGeoWithin( Circle.valueOf( [[50, 50], 100] ) )
Restaurant.findByLocationGeoWithin( Sphere.valueOf( [[50, 50], 0.06]) )
// native query
Restaurant.findByPointGeoWithin([ '$polygon': [ [0.0d, 0.0d], [3.0d, 0.0d], [3.0d, 3.0d], [0.0d, 3.0d], [0.0d, 0.0d] ] ])
Description

The $geoWithin operator is a geospatial query operator that queries for a defined point, line or shape that exists entirely within another defined shape. When determining inclusion, MongoDB considers the border of a shape to be part of the shape, subject to the precision of floating point numbers. See $geoWithin for more information.

findByNear

Purpose

Executes a MongoDB $near query

Examples
import grails.mongodb.geo.*
...
Restaurant.findByLocationNear( Point.valueOf( 40, 40 ) )
// native query
Restaurant.findAllByLocationNear( [$geometry: [type:'Point', coordinates: [1,7]], $maxDistance:30000] )
// criteria query
Restaurant.withCriteria {
    near 'location', Point.valueOf(1,7), 300000
}
Description

Specifies a point for which a geospatial query returns the closest documents first. The query sorts the documents from nearest to farthest. See $near documentation for more info.

findByNearSphere

Purpose

Executes a MongoDB $nearSphere query

Examples
import grails.mongodb.geo.*
...
Restaurant.findByLocationNearSphere( Point.valueOf( 40, 40 ) )
// native query
Restaurant.findAllByLocationNearSphere( [$geometry: [type:'Point', coordinates: [1,7]], $maxDistance:30000] )
// criteria query
Restaurant.withCriteria {
    nearSphere 'location', Point.valueOf(1,7), 300000
}
Description

Specifies a point for which a geospatial query returns the closest documents first. The query sorts the documents from nearest to farthest. MongoDB calculates distances for $nearSphere using spherical geometry.

See the documentation for the $nearSphere query operator.

findByWithinBox

Purpose

Executes a MongoDB $within query on legacy coordinate pairs

The $within operator is considered legacy and replaced by $geoWithin. Hence this method is deprecated and findByGeoWithin should be used instead
Examples
Hotel.findByLocationWithinBox( [[40, 30],[60, 70]] )
Hotel.findByLocationWithinBox( Box.valueOf([[40, 30],[60, 70]]) )

findByWithinCircle

Purpose

Executes a MongoDB $within query on legacy coordinate pairs

The $within operator is considered legacy and replaced by $geoWithin. Hence this method is deprecated and findByGeoWithin should be used instead
Examples
Hotel.findByLocationWithinCircle([[40, 30],40])
Hotel.findByLocationWithinCircle( Circle.valueOf( [[40, 30],40] ) )
Purpose

Executes a MongoDB $text search query

Examples
assert Product.search("coffee").size() == 5
assert Product.search("bake coffee cake").size() == 10
assert Product.search("bake coffee -cake").size() == 6
assert Product.search('"Coffee Cake"').size() == 1

assert Product.search('tarta', [language:'es', offset:5, max:10])
Description

The search method uses MongoDB’s full text search support to perform full text search on a "text" index.

searchTop

Purpose

Executes a MongoDB $text search query

Examples
assert Product.searchTop("coffee").size() == 5
assert Product.searchTop("coffee", 3)
Description

The searchTop method uses MongoDB’s full text search support to perform full text search on a "text" index with the results sorted by the MongoDB score. The method by default returns the top 5 results, but the second argument can be used to customize the number of results (top 3, top 10 etc.)

useCollection

Purpose

Allows switching which collection to use to persist for the domain class for the scope of the current session (connection).

Examples
Book.useCollection("non-fiction")
Description

The useCollection method allows switching, at runtime, the collection used persist and retrieve domain classes. The collectionName property will return the current collection being used. Note that the method switches the collection used for the scope of the current session/connection (ie. it is not permanent). If you wish to permanently change the collection used then you need to configure the mapping of the domain class.

useDatabase

Purpose

Allows switching which database to use to persist for the domain class for the scope of the current session (connection).

Examples
Book.useDatabase("non-fiction")
Description

The useDatabase method allows switching, at runtime, the database used persist and retrieve domain classes. The DB property will return the current database being used. Note that the method switches the database used for the scope of the current session/connection (ie. it is not permanent). If you wish to permanently change the database used then you need to configure the mapping of the domain class.

withCollection

Purpose

Allows switching which collection to use to persist for the domain class for the scope of the given closure

Examples
Book.withCollection("non-fiction") {
    // code here
}
Description

The useCollection method allows switching, at runtime, the collection used persist and retrieve domain classes. The collectionName property will return the current collection being used. Note that the method switches the collection used for the scope of given closure (ie. it is not permanent). If you wish to permanently change the collection used then you need to configure the mapping of the domain class.

withDatabase

Purpose

Allows switching which database to use to persist for the domain class for the scope of the given closure.

Examples
Book.withDatabase("non-fiction") {
    // code here
}
Description

The withDatabase method allows switching, at runtime, the database used persist and retrieve domain classes. The DB property will return the current database being used. Note that the method switches the database used for the scope of the given closure (ie. it is not permanent). If you wish to permanently change the database used then you need to configure the mapping of the domain class.