I went to build a new Grails-based app today, and I wanted to use SimpleDB as a backend (the app is an internal-use administration app, to configure a suite of AWS-deployed apps). So I went looking on how to use GORM with SimpleDB. This turned out to be a non-trivial task, so I thought I’d share the process with everyone.
Step 1: Install Grails.
I assume you know how to do that; if not, there’s a good installation guide over at the Grails site.
Step 2: Create an application
You do this using grails create-app
grails create-app helloworld
(All future commands will be run from within the newly-created application directory)
Step 3: Remove the Hibernate plugin.
After all, this is going to use SimpleDB as the backend, so we don’t need Hibernate. To do that, we edit the file grails-app/conf/BuildConfig.groovy
... grails.project.dependency.resolution = { ... plugins { ... // Don't need caching // compile ':cache:1.1.1' // plugins needed at runtime but not for compilation // Don't need Hibernate, as we'll use SimpleDB // runtime ":hibernate:3.6.10.7" // or ":hibernate4:4.1.11.6" ... } }
I’ve also disabled the caching plugin. This is fine for me, as I’m building an app for internal use and I don’t want caching. If you do want caching, however, you’ll need to track down some dependencies – the cache plugin requires some dependencies that were being provided by hibernate.
Step 4: Enable the SimpleDB GORM plugin.
So it turns out there’s an official SimpleDB plugin for GORM. All you have to do is drop a reference to it in your grails-app/conf/BuildConfig.groovy
file and you’re done! That was easy. Or was it?
plugins { ... runtime ":simpledb:0.5" }
$ grails run-app | Error Resolve error obtaining dependencies: The following artifacts could not be resolved: org.grails:grails-datastore-web:jar:1.0.0.RELEASE, org.grails:grails-datastore-gorm-plugin-support:jar:1.0.0.RELEASE: Could not find artifact org.grails:grails-datastore-web:jar:1.0.0.RELEASE in grailsCentral (http://repo.grails.org/grails/plugins) (Use --stacktrace to see the full trace)
Bummer – this version of the SimpleDB plugin needs an older version of the grails-datastore-web library – one that isn’t published on Grails Central or Maven Central. Well, that’s easily fixed, by editing the grails-app/conf/BuildConfig.groovy
file again:
dependencies { ... runtime 'org.grails:grails-datastore-web:jar:2.0.7.RELEASE' runtime 'org.grails:grails-datastore-gorm-plugin-support:jar:2.0.7.RELEASE' }
Step 5: Creating your first Domain Class
Now, when you run the app, you get this error:
$ grails run-app | Error 2014-01-28 23:40:54,441 [localhost-startStop-1] ERROR context.GrailsContextLoader - Error initializing the application: Cannot invoke method listDomains() on null object Message: Cannot invoke method listDomains() on null object Line | Method ->> 98 | doCall in org.grails.datastore.gorm.simpledb.plugin.support.SimpleDBApplicationContextConfigurer$_closure2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 51 | configure in org.grails.datastore.gorm.simpledb.plugin.support.SimpleDBApplicationContextConfigurer | 52 | doCall . in SimpledbGrailsPlugin$_closure2 | 262 | run in java.util.concurrent.FutureTask | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run . . . in java.lang.Thread
This isn’t that bad, though – if you have access to the source code, you can find out that this is because the SimpleDB template isn’t setup. Further reading of the code reveals that the setup is only done if there is a domain object. (Guess that’s a bug that was missed – but yay for open source for letting me figure that out). So we need to make a domain object – which we do by calling grails create-domain-class Book
.
So we’re good to go, right? No.
Step 6: Updating the SimpleDB plugin
Unfortunately, you now get this error:
| Error 2014-01-28 23:47:52,786 [localhost-startStop-1] ERROR context.GrailsContextLoader - Error initializing the application: Error creating bean with name 'transactionManagerPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbTransactionManager': Cannot resolve reference to bean 'simpledbDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbDatastore': Cannot resolve reference to bean 'simpledbMappingContext' while setting bean property 'mappingContext'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbMappingContext': FactoryBean threw exception on object creation; nested exception is java.lang.NoSuchMethodError: org.grails.datastore.mapping.model.MappingFactory.createMappedForm(Lorg/grails/datastore/mapping/model/PersistentEntity;)Ljava/lang/Object; Message: Error creating bean with name 'transactionManagerPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbTransactionManager': Cannot resolve reference to bean 'simpledbDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbDatastore': Cannot resolve reference to bean 'simpledbMappingContext' while setting bean property 'mappingContext'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbMappingContext': FactoryBean threw exception on object creation; nested exception is java.lang.NoSuchMethodError: org.grails.datastore.mapping.model.MappingFactory.createMappedForm(Lorg/grails/datastore/mapping/model/PersistentEntity;)Ljava/lang/Object; Line | Method ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by BeanCreationException: Error creating bean with name 'simpledbTransactionManager': Cannot resolve reference to bean 'simpledbDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbDatastore': Cannot resolve reference to bean 'simpledbMappingContext' while setting bean property 'mappingContext'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbMappingContext': FactoryBean threw exception on object creation; nested exception is java.lang.NoSuchMethodError: org.grails.datastore.mapping.model.MappingFactory.createMappedForm(Lorg/grails/datastore/mapping/model/PersistentEntity;)Ljava/lang/Object; ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by BeanCreationException: Error creating bean with name 'simpledbDatastore': Cannot resolve reference to bean 'simpledbMappingContext' while setting bean property 'mappingContext'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbMappingContext': FactoryBean threw exception on object creation; nested exception is java.lang.NoSuchMethodError: org.grails.datastore.mapping.model.MappingFactory.createMappedForm(Lorg/grails/datastore/mapping/model/PersistentEntity;)Ljava/lang/Object; ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by BeanCreationException: Error creating bean with name 'simpledbMappingContext': FactoryBean threw exception on object creation; nested exception is java.lang.NoSuchMethodError: org.grails.datastore.mapping.model.MappingFactory.createMappedForm(Lorg/grails/datastore/mapping/model/PersistentEntity;)Ljava/lang/Object; ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by NoSuchMethodError: org.grails.datastore.mapping.model.MappingFactory.createMappedForm(Lorg/grails/datastore/mapping/model/PersistentEntity;)Ljava/lang/Object; ->> 52 | createPersistentEntity in org.grails.datastore.mapping.simpledb.config.SimpleDBMappingContext - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 213 | addPersistentEntityInternal in org.grails.datastore.mapping.model.AbstractMappingContext | 206 | addPersistentEntity in '' | 63 | getObject in org.grails.datastore.gorm.bean.factory.AbstractMappingContextFactoryBean | 262 | run . . . in java.util.concurrent.FutureTask | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread | Error Forked Grails VM exited with error
It turns out that the published version of the SimpleDB plugin is too old to work with the newer GORM code. This isn’t surprising, but it’s annoying. To solve this – at least until a new version gets published – we need to build our own. Good thing that source code was available!
# Clone the Grails Data Mapping repository $ git clone https://github.com/grails/grails-data-mapping.git Cloning into 'grails-data-mapping'... remote: Counting objects: 51046, done. remote: Compressing objects: 100% (20296/20296), done. remote: Total 51046 (delta 13750), reused 51046 (delta 13750) Receiving objects: 100% (51046/51046), 15.74 MiB | 308.00 KiB/s, done. Resolving deltas: 100% (13750/13750), done. Checking connectivity... done. $ cd grails-data-mapping/ # Switch to the latest stable tag - v2.0.7 at this time. $ git checkout v2.0.7 Note: checking out 'v2.0.7'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b new_branch_name HEAD is now at 3e91b6d... change version to 2.0.7 release 23:56:53-robertdw~/util/grails-data-mapping ((v2.0.7)) # Build the project. $ export GRADLE_OPTS="-Xmx512m -XX:MaxPermSize=512m" # you need more permgen memory than the default $ ./gradlew install ... lots of output. Takes a bit of time, as it pulls down the internet to get the dependencies it needs... ... Note that you will get errors from the hibernate plugin. That doesn't matter for what we're doing here... # Copy the two libraries that we need to the lib directory of our project $ cp grails-datastore-simpledb/build/libs/grails-datastore-simpledb-0.6.BUILD-SNAPSHOT.jar ~/tmp/helloworld/lib/ $ cp grails-datastore-gorm-simpledb/build/libs/grails-datastore-gorm-simpledb-0.6.BUILD-SNAPSHOT.jar ~/tmp/helloworld/lib/ # Go into the grails-plugin directory and build the SimpleDB plugin $ cd grails-plugins/simpledb/ # You need to upgrade the plugin, unless you're using an older version of Grails. Just one of the quirks of Grails $ grails upgrade $ grails package-plugin --binary $ cp target/grails-plugin-simpledb-0.6.BUILD-SNAPSHOT.jar ~/tmp/helloworld/lib/ $ cd ~/tmp/helloworld
Remove the reference to the older version of the SimpleDB plugin from the BuildConfig.groovy
file, and it’s time to move on to the next error!
Step 7: Setup AWS Configuration
Which is this one:
$ grails run-app | Uninstalled plugin [simpledb] | Running Grails application objc[69323]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/bin/java and /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined. | Error 2014-01-29 00:14:22,617 [localhost-startStop-1] ERROR context.GrailsContextLoader - Error initializing the application: Error creating bean with name 'transactionManagerPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbTransactionManager': Cannot resolve reference to bean 'simpledbDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbDatastore': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalArgumentException: Please provide accessKey and secretKey Message: Error creating bean with name 'transactionManagerPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbTransactionManager': Cannot resolve reference to bean 'simpledbDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbDatastore': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalArgumentException: Please provide accessKey and secretKey Line | Method ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by BeanCreationException: Error creating bean with name 'simpledbTransactionManager': Cannot resolve reference to bean 'simpledbDatastore' while setting bean property 'datastore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpledbDatastore': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalArgumentException: Please provide accessKey and secretKey ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by BeanCreationException: Error creating bean with name 'simpledbDatastore': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalArgumentException: Please provide accessKey and secretKey ->> 262 | run in java.util.concurrent.FutureTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread Caused by IllegalArgumentException: Please provide accessKey and secretKey ->> 65 | <init> in org.grails.datastore.mapping.simpledb.util.SimpleDBTemplateImpl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 138 | createSimpleDBTemplate in org.grails.datastore.mapping.simpledb.SimpleDBDatastore | 123 | afterPropertiesSet in '' | 49 | getObject in org.grails.datastore.gorm.simpledb.bean.factory.SimpleDBDatastoreFactoryBean | 262 | run . . . in java.util.concurrent.FutureTask | 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor | 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker ^ 744 | run in java.lang.Thread | Error Forked Grails VM exited with error
This error is simple enough – we need to provide the AWS identifiers. Unfortunately, the plugin doesn’t follow the convention of getting the environment variables AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
. Still, it’s not hard to configure that. Off to the grails-app/conf/Config.groovy
file we go:
environments { development { grails { simpledb { accessKey = System.getenv('AWS_ACCESS_KEY_ID') secretKey = System.getenv('AWS_SECRET_ACCESS_KEY') domainNamePrefix = "${System.properties.getProperty('user.name')}_bookworld_" //this setting is optional, used when the same AWS account is shared between more than one environment dbCreate = 'drop-create' // one of 'drop, 'create', 'drop-create' } } } production { grails { simpledb { accessKey = System.getenv('AWS_ACCESS_KEY_ID') secretKey = System.getenv('AWS_SECRET_ACCESS_KEY') domainNamePrefix = 'PROD_' //this setting is optional, used when the same AWS account is shared between more than one environment dbCreate = 'create' // one of 'drop, 'create', 'drop-create' } } } } We're also going to need to have the <a href="http://aws.amazon.com/sdkforjava/>AWS SDK for Java</a> on the class path. To do that, I've chosen to use the <a href="http://grails.org/plugin/aws-sdk">Grails AWS-SDK plugin</a>, which gives a nice Groovy facade to the AWS classes. Back to the <code>BuildConfig.groovy</code> file. plugins { ... runtime ':aws-sdk:1.6.12' }
And now the application starts!
Step 8: Getting data in
So now to get some data in. Let's modify the previously-created Book class - we'll give it a title and an author. Also, because the default ID generator creates UUIDs, we need to declare the type of the id field to be a string. That gives us a class like this:
package helloworld class Book { static constraints = { } String id String title String author }
And then we can add some data into the Bootstrap.groovy file
import helloworld.Book class BootStrap { def init = { servletContext -> if (!Book.count()) { println 'Creating test data' new Book(title: 'Alice in Wonderland', author: 'Lewis Carroll').save(failOnError: true) new Book(title: 'The Wizard of Oz', author: 'Frank L. Baum').save(failOnError: true) } } def destroy = { } }
Quickly restarting the server - and creating a REST-style controller using grails generate-controller helloworld.Book
, we can then prove that the data is present by looking at the controller page - in the JSON format.
$ curl http://localhost:8080/helloworld/book/index.json [{"class":"helloworld.Book","id":"ef2cd65b-3fb6-435f-9fe0-ad0bdf0be3c3", "author":"Lewis Carroll","title":"Alice in Wonderland"}, {"class":"helloworld.Book","id":"88da9cdd-1dce-45ff-a9a5-24a3cb84aec8", "author":"Frank L. Baum","title":"The Wizard of Oz"}]
And that is Grails, using SimpleDB via GORM.
After all that, though, I ended up not using it, as it doesn’t support multi-value attributes (aka ‘sets’).
Instead, I wrote a thin service that uses the AWS SDK plugin I mentioned at the end.