1. Introduction

GORM, the object mapping technology built into the Grails framework, has traditionally been a primarily blocking API.

Async features were added in GORM 4, however these are merely a way to isolate your blocking operations onto a different thread and not a truly non-blocking implementation.

RxGORM builds ontop of RxJava and provides a completely non-blocking, stateless implementation of GORM.

Currently only MongoDB is supported as a backing store, however implementations are planned for SQL, REST client and other NoSQL datastores in the future (subject to driver support)

2. Getting Started

2.1. Installation

2.1.1. Installation using Grails

To get started with RxGORM for MongoDB in a Grails application simply add the following dependency to your build.gradle file:

build.gradle
compile "org.grails.plugins:rx-mongodb:6.1.5.RELEASE"

2.1.2. Installation without Grails

If you are not developing a Grails application then define the following dependency in your build.gradle or pom.xml:

build.gradle
compile "org.grails:grails-datastore-gorm-rx-mongodb:6.1.5.RELEASE"

With that done you will need to place the following logic in the bootstrap path of your application:

import org.grails.datastore.rx.mongodb.*
import com.mongodb.rx.client.MongoClient
...
MongoClient mongoClient = ... // create the MongoClient

new RxMongoDatastoreClient(
            mongoClient, (1)
            "myDatabase", (2)
            MyClass (3)
)
1 The MongoClient instance
2 The name of the default database to save objects to
3 One or many classes that implement RxEntity

The org.grails.datastore.rx.mongodb.RxMongoDatastoreClient class is required to initialize RxGORM for MongoDB.

2.2. Creating Domain Classes

When using RxGORM each entity that you persist to the database is known as a domain class.

If you are using RxGORM within Grails you can create a domain class with the create-domain-class command provided by the command line. Alternatively if you are not using Grails simply creating a Groovy class with src/main/groovy of your project will suffice.

A domain class must as a minimum implement the RxEntity trait.

However, when using RxGORM for MongoDB you should instead implement the grails.gorm.rx.mongodb.RxMongoEntity trait. For example:

package example

import grails.gorm.rx.mongodb.*
import org.bson.types.ObjectId

class Book implements RxMongoEntity<Book> {
    ObjectId id
    String title
}
The type is provided as a generic argument to the RxMongoEntity trait. This is important to ensure the correct return signatures are produced for methods provided by the trait.

In addition, for MongoDB an id of type ObjectId is required.

For more information on domain modelling in GORM, take a look at the GORM user guide documentation on the subject.

2.3. Performing CRUD Operations

Create, Read, Update and Delete (or CRUD) are the most common use cases when working with databases. The following examples will work you through how to perform each.

2.3.1. Create - Saving domain instances

To create and save a new instance to the database you should use the save() method:

new Book(title:"The Stand")
        .save()
        .toBlocking()
        .first()

The save() method returns an rx.Observable.

Notice that in this example we are calling toBlocking(), this is purely for demonstration and testing purposes to ensure the operation completes immediately.

In a real world application you would typically subscribe to the observable in order to receive a result:

new Book(title:"The Stand")
        .save()
        .subscribe { Book it ->
    println "Title = ${it.title}"
}
If you don’t call subscribe(..) or convert the Observable into a blocking operation, then no query will be executed! At least one call to subscribe(..) is required.

2.3.2. Read - Retrieving domain instances

To retrieve an instance you can use the static get(..) method:

Book.get(id)
    .subscribe { Book it ->
    println "Title = ${it.title}"
}

2.3.3. Update - Updating instances

To update an instance after retreiving it you can use the Observable.switchMap method:

Book.get(id)
    .switchMap() { Book it ->
    it.title = "The Shining"
    it.save()
}.subscribe { Book updated ->
    println "Book updated!"
}

The switchMap transforms an Observable and converters the result into another Observable. However, this is not the most efficient way to perform updates as you can use where queries to update an instance without retrieving it:

Book.where {
    id == id
}.updateAll(title:"The Shining")
 .subscribe { Number updateCount ->
    println "${updateCount} books were updated!"
}

2.3.4. Delete - Deleting instances

To delete an instance after retreiving it you can use the Observable.switchMap method:

Book.get(id)
    .switchMap() { Book it ->
    it.delete()
}.subscribe { Boolean wasDeleted ->
    if(wasDeleted) {
        println "Book deleted!"
    }
}

But, once again it is more efficient with where queries:

Book.where {
    id == id
}.deleteAll()
 .subscribe { Number deleteCount ->
    println "${deleteCount} books were deleted!"
}

3. Configuration

If you are using Grails then configuration can be provided via the grails-app/conf/application.yml file.

An example configuration can be seen below:

grails:
    mongodb:
        host: mycompany
        port: 1234
        username: foo
        password: bar
        options:
            clusterSettings:
                maxWaitQueueSize: 10

You can specify any setting of the MongoClientSettings within the options block. The example above is configuring the maxWaitQueueSize property.

An alternative to specifying the host and port is to use a MongoDB ConnectionString:

grails:
    mongodb:
        connectionString:  mongodb://user:[email protected]:27017

If you are not using Grails you can either provide a MongoClient instance directly in the constructor or you can supply an instance of PropertyResolver with the necessary configuration.

For example:

import org.springframework.core.env.*

def env = new StandardEnvironment()
env.getPropertySources().addLast(new PropertiesPropertySource("myConfig", myConfig))

new RxMongoDatastoreClient(
            env,
            "myDatabase",
            MyClass
)

If you are using Spring Boot then the Environment instance can be obtained from the ApplicationContext:

new RxMongoDatastoreClient(
            applicationContext.getEnvironment(),
            "myDatabase",
            MyClass
)

4. Querying

RxGORM supports all the typical ways of querying that you are used to with GORM including:

The major difference is that all query operations return an Observable. In this section we will go through the various ways you can query for GORM objects.

4.1. Basic Querying

To query for a single instance by identifier you can use the get(id) method:

Book.get(id)
    .subscribe { Book it ->
    println "Title = ${it.title}"
}

To query for a List of objects you can use the list() method:

Book.list()
    .subscribe { List<Book> books ->
    for(book in books)
        println "Title = ${book.title}"
}

However, for large data sets it is better to use the findAll() method which will return each result in a reactive manner rather than load the entire list into memory:

Book.findAll()
    .subscribe { Book it ->
    println "Title = ${it.title}"
}

To return the first result you can use the first() method:

Book.first()
    .subscribe { Book it ->
    println "Title = ${it.title}"
}

And conversely to return the last result you can use the last() method:

Book.last()
    .subscribe { Book it ->
    println "Title = ${it.title}"
}

4.2. Where Queries

Where queries are the preferred way to build more complex queries. They are type safe, expressive and lazily executed:

def query = Book.where {
    title == 'The Stand'
}

Note that no query is actually executed directly, instead the where(Closure) method returns an instance of DetachedCriteria.

Like an rx.Observable, the DetachedCriteria class can be subscribed to:

Book.where {
    title == 'The Stand'
}.subscribe { Book it ->
    println "Title = ${it.title}"
}

Alternatively you can execute one of the query methods to invoke the query and return an rx.Observable:

Book.where {
    title == 'The Stand'
}.list().subscribe { List<Book> books ->
    for(book in books)
        println "Title = ${book.title}"
}

For more information on the syntax of where queries see the relevant section in the GORM documentation.

4.3. Dynamic Finders

Although Where queries are preferred, Dynamic finders are another option for simple queries and are also very expressive. The syntax for Dynamic finders is described in the GORM user guide, the major difference in RxGORM is that all dynamic finders return an rx.Observable:

Book.findByAuthor("Stephen King")
    .subscribe { Book it ->
    println "Title = ${it.title}"
}

5. Associations and Lazy Loading

Associations and how they are handled are the biggest difference between RxGORM and the regular blocking version of GORM. As a user it is import that you familiarize yourself with how associations are handled in RxGORM in order to effectively build reactive, non-blocking applications. The following sections will describe how to adapt your code to take into account associations and your data model.

5.1. Single Ended Associations

Consider for example a domain model such as:

package example
...
class Book implements RxMongoEntity<Book> {
    ObjectId id
    String title

    Author author
}

Each Book has an Author instance. The Author instance may have its own set of associations. In order to avoid potentially loading your entire object graph into memory RxGORM will load each Book instance with the author property loaded as a lazy proxy.

This is traditionally how most blocking object mapping implementations have worked. However, an issue arises in a reactive scenario when you access the proxy:

for(book in books) {
    println "Author: ${book.author.name}"
}

In the above example RxGORM has no choice but to block in order to load the association lazy and access the name! Typically you do not want your reactive application to block at any point, so in order to get around this each proxy loaded by GORM implements the ObservableProxy interface. That means the above code can be written in a non-blocking manner:

for(book in books) {
    book.author.subscribe { Author author ->
        println "Author: ${author.name}"
    }
}

Another alternative if you know you plan to access the author association, is to use an eager GORM query. In this case GORM will fetch and initialize the association in a non-blocking manner:

// using a dynamic finder
Book.findByTitle("The Stand", [fetch:[author:"eager"]])

// using a where query
Book.where {
    title == 'The Stand'
}.join('author')

5.2. Many Ended Associations

In the previous section we discussed the implications of association proxies for single-ended associations, however the same applies with many-ended associations.

In the case of collection types, these implement the RxPersistentCollection and again can be subscribed to:

Author author == ... // load author
author.books.subscribe { Collection<Book> books ->
    for(Book book in books)
        println "Book: ${book.title}"
}

Like single-ended associations you can also use a join query to eagerly fetch the association and avoid having to subscribe to it in order to achieve a non-blocking outcome.