3 Mapping Domain Classes to MongoDB Collections - Reference Documentation
Authors: Graeme Rocher, Burt Beckwith
Version: 5.0.8.RELEASE
Table of Contents
3 Mapping Domain Classes to MongoDB Collections
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]
}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']
}class Person {
String firstName
String lastName
Address address
List otherAddresses
static embedded = ['address', 'otherAddresses']
}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)Customized Collection and Database Mapping
You may wish to customize how a domain class maps onto aMongoCollection. This is possible using the mapping block as follows:class Person {
..
static mapping = {
collection "mycollection"
database "mydb"
}
}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"
}
}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 {}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
}import org.bson.types.ObjectIdclass Person {
ObjectId id
}ObjectId instances are generated in a similar fashion to UUIDs.Assigned Identifiers
Note that if you manually assign an identifier, then you will need to use theinsert 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
}
…
def p = new Person(id:"Fred")
// to insert
p.insert()
// to update
p.save()3.2 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 }
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.One of the implications of this is if you override the collection with a non-dirty checking aware type it can disable
dirty checking and prevent the property from being updated.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')3.3 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
}
}indexAttributes configuration parameter:class Person {
String name
static mapping = {
name index:true, indexAttributes: [unique:true, dropDups:true]
}
}hint argument to any dynamic finder:def people = Person.findByName("Bob", [hint:[name:1]])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 thecompoundIndex mapping:class Person {
…
static mapping = {
compoundIndex name:1, age:-1
}
}Indexing using the 'index' method
In addition to the convenience features described above you can use theindex method to define any index you want. For example:static mapping = { index( ["person.address.postCode":1], [unique:true] ) }
index method get passed to the underlying MongoDB createIndex method.
3.4 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 theWriteConcern for a domain class you can do so in the mapping block:import com.mongodb.WriteConcernclass 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.5 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
}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']
Document instance that gets persisted to the MongoDB allowing for more dynamic domain models.
3.6 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.3.6.1 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' } }
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:new Restaurant(id:"Dan's Burgers", location: new Point(50, 50)).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
Pointis within aBox,Polygon,CircleorSphere - findBy...GeoIntersects - Find out whether a
Pointis within aBox,Polygon,CircleorSphere - findBy...Near - Find out whether any GeoJSON
Shapeis near the givenPoint - findBy...NearSphere - Find out whether any GeoJSON
Shapeis near the givenPointusing spherical geometry.
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 aSpherediffers from aCirclein that the radius is specified in radians. There is a specialDistanceclass that can help with radian calculation.
Native Querying Support
In addition to being able to pass anyShape 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] )
3.6.2 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 thegeoIndex mapping:class Hotel {
String name
List location static mapping = {
location geoIndex:'2d'
}
}indexAttributesclass Hotel {
String name
List location static mapping = {
location geoIndex:'2d', indexAttributes:[min:-500, max:500]
}
}new Hotel(name:"Hilton", location:[50, 50]).save()
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'
def box = [[40.73083d, -73.99756d], [40.741404d, -73.988135d]] def h = Hotel.findByLocationWithinBox(box)
def center = [50, 50] def radius = 10 def h = Hotel.findByLocationWithinCircle([center, radius])
class Hotel {
String name
List location
int stars static mapping = {
compoundIndex location:"2d", stars:1
}
}Hotel has.
3.6.3 GeoJSON Data Models
You can also store any GeoJSON shape using thegrails.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()
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]] ) )
3.7 Full Text Search
Using MongoDB 2.6 and above you can create full text search indices.To create a "text" index using theindex method inside the mapping block:class Product {
ObjectId id
String title static mapping = {
index title:"text"
}
}search method:assert Product.search("bake coffee cake").size() == 10 assert Product.search("bake coffee -cake").size() == 6
searchTop method:assert Product.searchTop("cake").size() == 4 assert Product.searchTop("cake",3).size() == 3
countHits method:assert Product.countHits('coffee') == 53.8 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/domainIf 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 org.bson.* 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, Document, Document>(Birthday) { @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 } })
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 }
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 }
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") } }
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
BirthdayType add the following to grails-app/conf/spring/resources.groovy:import com.example.BirthdayType// Place your Spring DSL code here
beans = {
birthdayType(BirthdayType)
}