(Quick Reference)

3 Mapping Domain Classes to Mongo Collections - Reference Documentation

Authors: Graeme Rocher, Burt Beckwith

Version: 1.3.0.GA

3 Mapping Domain Classes to Mongo Collections

Basic Mapping

The way GORM for Mongo 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 Mongo DBCollection called "person".

Embedded Documents

It is quite common in Mongo 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']
}

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 DBCollection. 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"
    }
}

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
    }
}

3.1 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.

3.2 Indexing Queries

Basics

Mongo 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 Mongo 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:["firstName":1]
}

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.

3.3 Customizing the WriteConcern

A feature of Mongo 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.

3.4 Dynamic Attributes

Unlike a relational database, Mongo 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 Mongo document. Using GORM for Mongo 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 DBObject instance that gets persisted to the Mongo allowing for more dynamic domain models.

3.5 Dynamic Database or Collection Switching

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.

3.6 Geospacial Querying

It is possible to use Mongo's Geospacial querying capability by mapping a list or map property using the geoIndex mapping:

class Hotel {
    String name
    List location

static mapping = { location geoIndex:true } }

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:true, 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 Mongo 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.

3.7 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. 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 com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
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;

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

@Override protected void queryInternal(PersistentProperty property, String key, Query.PropertyCriterion criterion, DBObject 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, DBObject 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, DBObject 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, DBObject 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.BirthdayType

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

3.8 Stateful vs. Stateless Domain Classes

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 a an update or an insert and act appropriately
  • GORM store the state of the read MongoDB document and therefore updates to schemaless properties don't require an extra query
  • GORM can store the 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:

def b = new Book(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.