Blogs

SpringSource Blog

GORM for MongoDB: New Milestone, Richer Experience

grocher

Last year we introduced support for MongoDB in GORM (along with many other GORM implementations) and it has been extremely well received by the community. We have had a ton of feedback, and today we are pleased to announce a new release (Milestone 2) which addresses some of the feedback we have received.

Embedded Document Support

The number one requested feature was nested document support and in this release we have improved that significantly. Now you can embed other domains using the standard embedded mapping in GORM:

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

The embedded domains get stored in a nested document within the primary Mongo document. In addition, lists and maps of basic types can now also be persisted to native Mongo ArrayList and hashes:

class Person {
	List<String> friends
	Map pets
}
...

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

Both embedded domains and lists and maps get stored within the primary Mongo document for the domain allowing more of the common MongoDB patterns to be implemented using GORM.

Geospacial Indexing and Querying

MongoDB has native support for Geospacial indexing and querying and this is now supported in GORM for MongoDB. You can define a list or map as being "geo-indexed":

class Hotel {
	String name
	List location
	static mapping = {
		location geoIndex:true
	}
}

And then easily persist the geo data using a two-dimensial list representing latitude and longitude:

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

Alternatively, and possibly more readable, you can use a map containing the latitude and longitude values:

new Hotel(name:"Hilton", location:[lat: 40.739037, long: 73.992964]).save()

Once persisted a domain class can then be queried with the new findBy*Near syntax:

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

You can also use bound queries to locate a position within a rectangle using the findBy*WithinBox method :

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

Or within a circle using the findBy*WithinCircle method:

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

Schemaless Domain Models

MongoDB is completely schemaless meaning you are not limited to a fixed number of columns like in a relational database. GORM for MongoDB now supports schemaless domain models. You can continue to specify your fixed domain properties inside your domain model:

class Plant {
    boolean goesInPatch
    String name
}

However, you can now also persist additional properties using the Groovy subscript operator:

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

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

There are many more improvements including the ability to customize index creation, support for query-by-example and more complete support for the GORM API. The documentation has been updated to cover all these new features. Let us know what you think, your feedback is invaluable.

One final plug, if you are in the Madrid area and want to know more about GORM for MongoDB come down to the Spring IO conference this Thursday where there will be many more talks on Spring, Grails and GORM.

Similar Posts

Share this Post
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • DZone
  • LinkedIn
  • Slashdot
  • Technorati
  • Twitter
 

44 responses


  1. Broke a lot of things. Transaction don't work anymore. read only properties too.


  2. @Laurent Maybe if you can provide more details in a JIRA ( http://jira.codehaus.org/browse/GRAILSPLUGINS ) that would help. We have tests covering transaction support, it may be something specific to your application. I'm not sure what you mean by read-only properties, an example would help.


  3. I am a couple of days late to file this issue. Two others (Redis and Riak) have been changed their been names to 'springDatastore'. I think it would be great to have the same bean name as those two.


  4. @Chanwit We changed the bean names to avoid conflicts in the case where you have both the mongodb and redis plugins installed. If all plugins used 'springDatastore' there would be conflicts.


  5. Thanks, Graeme. This seems to be a design decision I overlooked.


  6. I am still having issues with loading a document from the database that has an association, editing it, and saving it back. It fails trying to persist a BSONObject. If I manually delete the associated document from Mongo, it proceeds. I assume it's doing eager fetching and the associated document is left as its raw type when brought in by the driver?

    Dynamic finders are also not working with properties mapped with 'attribute'. I know this one is undocumented but I thought I would throw it out there.

    Great work on the improvements though! Embedded documents and dynamic properties are nice additions.


  7. @Xristofer Can you report issues and attach examples of the problems you are seeing: JIRA ( http://jira.codehaus.org/browse/GRAILSPLUGINS )

    Thanks!


  8. Thanks for the update!
    Unfortunately, it breaks down the code.

    1) Read-only properties like spring-security-core User code lead to compile errors:

    Set getAuthorities() {
    UserRole.findAllByUser(this).collect { it.role } as Set
    }

    I've added the setter to allow compilation go ahead:

    void setAuthorities(Set authorities){
    authorities.each {
    UserRole.create(this, it)
    }
    }

    2) For some reason

    assert Domain.("hasMany" property).contains("object it really do contain")

    always leads to a failure. Changed to .any{it.id==obj.id}.

    3) Transactions doesn't work.

    Exception Message: Already value [org.springframework.datastore.mapping.transactions.SessionHolder@ba117b] for key [org.springframework.datastore.mapping.mongo.MongoDatastore@380f83] bound to thread [http-8080-3]
    Caused by: Could not open Datastore Session for transaction; nested exception is java.lang.IllegalStateException: Already value [org.springframework.datastore.mapping.transactions.SessionHolder@ba117b] for key [org.springframework.datastore.mapping.mongo.MongoDatastore@380f83] bound to thread [http-8080-3]

    So I've added

    static transactional = false

    to all my services. Also I have commented all "Domain.withTransaction" statements.

    4) Even after that I'm able to run tests and run project in development environment, but cannot deploy. Using Tomcat 7, Grails 1.3.6.

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'messageSource': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mongoTransactionManager': Cannot resolve reference to bean 'mongoDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mongoDatastore': FactoryBean threw exception on object creation; nested exception is java.lang.NoSuchMethodError: org.springframework.data.document.mongodb.MongoTemplate.execute(Lorg/springframework/data/document/mongodb/DbCallback;)Ljava/lang/Object;

    Thou I'd better revert to M1 version. Or am I just doing something wrong?


  9. @Dmitry Can you raise JIRA issues with examples attached for the issues you are seeing? That feedback would be much appreciated. A simple test of withTransaction seems to work for me.

    The last error you have seems to be due to conflicting versions of spring-data-mongo. Can you check of your WEB-INF/lib directory and make sure you only have M1 of the spring-data-mongo? Thanks.


  10. @Dimitry you can find how to use Spring security core and mongodb here : https://github.com/lgaches/grails-mongospringsecurity


  11. @grocher just have it done.
    I have two spring-data-mongodb jars in WEB-INF/lib even if I only create a new blank application without Hibernate and with Mongo, can't figure it out.

    @Laurent it looks like this code won't work: try to cover UserRole with tests fully. HQL doesn't work with MongoDB.


  12. Many thanks, now I don't have to mess with that dynamic properties plugin! I think this really unleashes the power of a schema-less database when you can persist dynamic stuff.


  13. I guess a similar problem as with the M1 occurs – the class MongoOptionsFactoryBean could not be found:

    Error executing script Console: No such property: MongoOptionsFactoryBean for class: MongodbGrailsPlugin
    No such property: MongoOptionsFactoryBean for class: MongodbGrailsPlugin

    Any idea on how to fix this?


  14. @grocher, my issues appear to be IDE related. I switched to IntelliJ from STS as of my last post and the issue goes away even using the same project directory.


  15. Thanks again!

    I'm on M4 now, so far works pretty well, although I've just started the project…

    Anyway, couple of comments for this version, which I consider would be a nice to have:

    - Embedded document of ObjectId type. So far it works only with embedded docs that are existing domains (created with create-domain-class), however it doesn't work if I have an ObjectId property and trying to save it, I'm getting a NPE. This is useful if someone decided to use ObjectId as domain's class id and want to save it as a reference within another domain class. For now String id is enough, but allowing ObjectId id is pretty useless.

    - Atomic operations. Not sure if or how they'll fit into Gorm, but I think it would be nice to have at least some of them implemented. For example: instead of 'User.collection.update([_id: session.auth.id], [$inc: [invitesLeft: -1]])' to have something like 'User.incInvitesLeft(id)' or similar.

    Again, thanks a lot for richer experience.


  16. M4 really allows deployment without compile fails. But it's still not production-ready.
    - Sometimes Mongo objects are returned instead of domains (in production mode). In my case, it is simple "hasMany" association. In development it's okay.
    - Transactions doesn't work. When in UserDetailsService I type
    User.withTransaction { status ->
    It fails with session error:

    org.springframework.transaction.CannotCreateTransactionException: Could not open Datastore Session for transaction; nested exception is java.lang.IllegalStateException: Already value [org.springframework.datastore.mapping.transactions.SessionHolder@2eb89c06] for key [org.springframework.datastore.mapping.mongo.MongoDatastore@7248989f] bound to thread [http-8080-3]

    at org.springframework.datastore.mapping.transactions.DatastoreTransactionManager.doBegin(DatastoreTransactionManager.java:148)

    Caused by: java.lang.IllegalStateException: Already value [org.springframework.datastore.mapping.transactions.SessionHolder@2eb89c06] for key [org.springframework.datastore.mapping.mongo.MongoDatastore@7248989f] bound to thread [http-8080-3]
    at org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(TransactionSynchronizationManager.java:179)
    at org.springframework.datastore.mapping.transactions.DatastoreTransactionManager.doBegin(DatastoreTransactionManager.java:129)
    … 79 more

    Hope production-ready plugin will be available soon. All of us need it so much.
    And one question. Is there any method to get all possible keys of a domain object? For [...] syntax.

    Thanks for a fast bug-fixing!


  17. @grocher I'd be glad to to help you developing the plugin in any way if it is possible :)


  18. @Dmitry Contributions are welcome. Feel free to fork the repository at https://github.com/grails/inconsequential

    What would be good otherwise is some reproducible examples attached to the JIRAs you raised. That would really help in fixing the problems you have found so far.


  19. I'm having some issues with gorm's delete. It doesn't work!

    Test below always fails… the record/document gets created, but .delete() doesn't work!!! I've tried within a controller as well, no luck…

    Can anyone confirm this issue? Or I'm missing something?

    Thanks.

    PS: This integration test always fails..
    void test_page() {
    def p = new Page(title: "About", body: "body", isPublic:true).save(flush: true)
    def p2 = Page.get(p.id)
    assertNotNull p2
    p2.delete(flush:true)
    def p3 = Page.get(p.id)
    assertNotNull p3
    }


  20. I have created a JIRA issue with delete http://jira.codehaus.org/browse/GRAILSPLUGINS-2886

    Hope that helps.


  21. @grocher With M4 the dependency problem seems to be gone. Thanks!


  22. I'm not really sure, does the unique constraint should work?

    Should I create a JIRA issue….

    Thanks for the plugin


  23. @jjchiw

    Yes, it does, check indexAttributes

    This sample is from plugin's docs:

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


  24. @Octavian

    Hi, thanks for the information

    Even it works the mapping, the constraints in the Domain Model, it seems it does not works…

    When I do
    import org.bson.types.ObjectId

    class Foo {

    ObjectId id
    String name

    static constraints = {
    name nullable:false, blank:false, matches:"[\\d] ", unique:true
    }

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

    def f = new Foo(name:"456")
    f.validate() //it's always true

    So, should I create a JIRA issue?


  25. I've seen the problem with constraints as well. nullable fields are not allowed to be to set to null.


  26. From the mongodb website regarding null values which I am currently having problems with :
    Are null values allowed?
    For members of an object, yes. You cannot add null to a database collection though as null isn't an object. You can add {}, though.

    Perhaps the GORM api needs to be updated to set the value to {}?


  27. Would it be feasible/possible/convenient to have a "Gorm for db4o" ? http://developer.db4o.com


  28. @German Yes GORM for db4o is possible based on the existing infrastructure we have. Contributions welcome


  29. I have trouble finding the version 1.0.0.M2.

    There are 4 different versions in svn.codehaus.org, including:
    # RELEASE_1_0-M2/
    # RELEASE_1_0-M3/
    # RELEASE_1_0-M4/
    # RELEASE_1_0_0_M1/

    I'm not sure about which one is the version announced in this message.


  30. when is version 5 going to be available?


  31. @Imran Next week sometime

    @Jzhwu Use the M4 version for the moment until M5 is out


  32. Thanks! I've tried the new features, which is exactly what I was looking for. Well done!


  33. I'm new to Mongo GORM and many thanks to the posts here which answered the problems I have encountered, I'm using mongodb-1.0-M5 btw which still appears to have many of these problems.

    One thing I noticed is that domain classes are based on application name prefix in a package like structure, I see in the logs for example:

    Mon Apr 18 00:13:53 [conn11] building new index on { _id: 1 } for testwebapp.requestMap

    I was expecting the package name of the class to be be used so com.myapp.RequestMap for example

    Is this expected behavior?

    Thanks


  34. Another question:
    In your article's dynamic property demonstration, the object must be initialized with properties defined in Domain class to enable further dynamic properties:
    1 def p = new Plant(name:"Pineapple")
    2 p['color'] = 'Yellow'
    3 p['hasLeaves'] = true

    I tried to directly add dynamic properties to an object constructed without any parameter, like:
    1 def p = new Plant()
    2 p['color'] = 'Yellow'
    3 p['hasLeaves'] = true

    Executing above code failed, resulting in an exception:
    java.lang.IllegalStateException: Cannot obtain DBObject for transient instance, save a valid instance first
    at org.grails.datastore.gorm.mongo.MongoGormInstanceApi.getDbo(MongoGormEnhancer.groovy:120)
    at org.grails.datastore.gorm.mongo.MongoGormInstanceApi$getDbo.callCurrent(Unknown Source)
    at org.grails.datastore.gorm.mongo.MongoGormInstanceApi.putAt(MongoGormEnhancer.groovy:81)
    at org.grails.datastore.gorm.GormEnhancer$1.call(GormEnhancer.groovy:93)

    The fundamental problem is that I need to supply all the dynamic properties, which have been configured in another Domain class, together with pre-defined ones to create.gsp, so that user can input all the data items. Now it seems that dynamic properties can only be injected after pre-defined ones' values being set. Any solutions? Many thanks!


  35. Hi guys,

    I have not experience in Grails (1.3.6).

    Is it possible to save in db (Mongo) domain with embedded domain which contains domain as well?

    Thanks in advance.


  36. can save the List(no basic types) as embed documents?
    like:
    class User{
    String firstName
    List authorities
    static embedded = ['authorities']
    }

    class Role{
    String authority
    }


  37. does this and the redis plugin use spring data project under the covers?


  38. I´m trying to use the GORM´s example to embedded documents.

    class Person {

    static mapWith = "mongo"

    Address homeAddress
    Address workAddress
    static embedded = ['homeAddress', 'workAddress']
    }
    class Address {

    static mapWith = "mongo"
    String number
    String code
    }

    However, I get an error like : Cannot resolve reference to bean 'mongoMappingContext' while setting bean property 'mappingContext'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mongoMappingContext': FactoryBean threw exception on object creation; nested exception is org.springframework.datastore.mapping.model.IllegalMappingException: Mapped identifier [id] for class [Address] is not a valid property

    I have the M6 version of mongoDB plugin for grails…


  39. @Simon Yes you can ask of M6 release

    @numan

    You may have run into http://jira.grails.org/browse/GPMONGODB-21

    See the workaround in that issue.

    If the problem persists please report an issue at http://jira.grails.org/browse/GPMONGODB


  40. When I try to assign my own id instead of generated id, the save fails with the following exception. Am I missing something here.
    I tried save(insert:true).. no joy.

    class Product {
    String id;
    String description;
    String internalName;
    String brandName;

    static mapping = {
    id generator:'assigned'
    }
    }

    java.lang.NullPointerException
    at org.grails.datastore.mapping.mongo.engine.MongoEntityPersister.isDirty(MongoEntityPersister.java:541)
    at org.grails.datastore.mapping.core.AbstractSession.isDirty(AbstractSession.java:257)
    at org.grails.datastore.mapping.engine.NativeEntryEntityPersister.persistEntity(NativeEntryEntityPersister.java:603)


  41. I have the same exact issue as Vikram G. Anyone else?


  42. jzhwu, I had the same problem that you did. If you initialize the object with a throwaway value from the domain class (I chose "name" and set it to a value that will never be referenced), then the object will work. This seems like a bug, but the workaround is so simple that I'm unlikely to log it.


  43. It seems, with 1.0.0.RC3, that embedded COLLECTIONS of objects work 0% of the time.

    I can do:

    SomeObject so
    static embedded = ['so']

    just fine.. but if I do:

    List so
    static embedded = ['so']

    a) the collection is null, so I have to manually instantiate it (not true with say, hasMany.. or not consistent).
    b) when I save the object, the embedded COLLECTION does not get saved (literally that entire 'key' is non-existant in mongo)


  44. Hi!
    Can I create a relationship between a mongo domain class and hibernate?

3 trackbacks

Leave a Reply