Mar 31st 2008 01:11 am Using Gant to execute a groovy script within the Grails context (updated)
In a previous post I showed a script that I had created to allow the execution of a groovy script within a grails application context (including access to domain objects, controllers, services, etc). A couple of people reported a problem with the script where they were getting lazy initialization exceptions. I finally tracked this issue down to one where many-to-many relationships are being used between two domain classes.
Here is an updated script that fixes this issue by setting up the hibernate session in the Gant script.
scripts/RunScript.groovy:
import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU import org.springframework.orm.hibernate3.SessionFactoryUtils import org.springframework.orm.hibernate3.SessionHolder import org.springframework.transaction.support.TransactionSynchronizationManager grailsHome = Ant.project.properties."environment.GRAILS_HOME" includeTargets << new File ( "${grailsHome}/scripts/Package.groovy" ) includeTargets << new File ( "${grailsHome}/scripts/Bootstrap.groovy" ) target('default': "Execute the specified script after starting up the application environment") { depends(checkVersion, configureProxy, packageApp, classpath) runScript() } target(runScript: "Main implementation that executes the specified script after starting up the application environment") { parseArguments() if (argsMap["params"].size() == 0) { event("StatusError", ["Required script name parameter is missing"]) System.exit 1 } compile() classLoader = new URLClassLoader([classesDir.toURL()] as URL[], rootLoader) Thread.currentThread().setContextClassLoader(classLoader) loadApp() configureApp() configureHibernateSession() argsMap["params"].each { scriptFile -> executeScript(scriptFile, classLoader) } } def configureHibernateSession() { // without this you'll get a lazy initialization exception when using a many-to-many relationship def sessionFactory = appCtx.getBean("sessionFactory") def session = SessionFactoryUtils.getSession(sessionFactory, true) TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)) } def executeScript(scriptFile, classLoader) { File script = new File(scriptFile) if (script.exists()) { def shell = new GroovyShell(classLoader, new Binding(ctx: appCtx, grailsApplication: grailsApp)) shell.evaluate(script.text) } else { event("StatusError", ["Designated script doesn't exist: $scriptFile"]) } } // this argument parsing target has actually been submitted as a patch to Init.groovy after some feedback // on the grails user mailing list and will hopefully be in the next release of grails. // Vote it up if you like it: http://jira.codehaus.org/browse/GRAILS-2663 argsMap = [params: []] target(parseArguments: "Parse the arguments passed on the command line") { args?.tokenize().each { token -> def nameValueSwitch = token =~ "--?(.*)=(.*)" if (nameValueSwitch.matches()) { // this token is a name/value pair (ex: --foo=bar or -z=qux) argsMap[nameValueSwitch[0][1]] = nameValueSwitch[0][2] } else { def nameOnlySwitch = token =~ "--?(.*)" if (nameOnlySwitch.matches()) { // this token is just a switch (ex: -force or --help) argsMap[nameOnlySwitch[0][1]] = true } else { // single item tokens, append in order to an array of params argsMap["params"] << token } } } event("StatusUpdate", ["Done parsing arguments: $argsMap"]) }
If you save that in your application’s scripts directory as RunScript.groovy you can execute it like this:
grails run-script [path-to-script-1] [path-to-script-2]...[path-to-script-n]
The paths are relative to the root of the grails application directory. To see it’s potential use, if you had a directory in your app called userScripts that contained a script createBook.groovy that exercises the many-to-many relationship:
def theTalisman = new Book(title: "The Talisman").save() def stephenKing = new Author(name:"Stephen King").addToBooks(theTalisman).save() def peterStraub = new Author(name:"Peter Straub").addToBooks(theTalisman).save()def book = Book.findByTitle("The Talisman") println "Found ${book.title} with authors = ${book.authors*.name}"
If you had these domain classes:
Author.groovy
class Author { static hasMany = [books:Book] String name }
Book.groovy
class Book { static belongsTo = Author static hasMany = [authors:Author] String title }
You could then run the script:
grails run-script userScripts/createBook.groovyAnd you’d see the output:
Found The Talisman with authors = ["Stephen King", "Peter Straub"]
Since you’re running in the grails context, you also have access to all of your service and controller classes. Anything that you can do inside grails can also be scripted using this target.
Without the new fix that configures the hibernate transaction on the session (configureHibernateSession()), you’d see this error instead:
failed to lazily initialize a collection of role: Book.authors, no session or session was closed
Thanks to Peter Wolf on the GUM mailing list for reporting an issue and testing things out as well as Armin Heinzer for the pointer to the suggested fix on the grails-user list.
Posted by tednaleid / command line and grails and groovy
Josh Reed on 11 Apr 2008 at 12:23 am #
Hey Ted,
Thanks for this work. I ran into a few problems when I was trying to cut and paste this into my Grails install.
1) On line 4, it looks like Wordpress ate a line break. It should end after: TransactionSynchronizationManager
2) Some of the double quotes got converted to nice HTML double quotes. When you cut and paste, make sure you just do a global search and replace to standard double quotes ”
3) Finally, the regex in the parseArguments target doesn’t work for me. If I type:
grails run-script userScripts/populateDatabase.groovy
This matches the nameOnlySwitch. Just looking at the regex, this makes sense to me because you have “-?(.*)” which I interpret as “zero or one dashes followed by anything”. If I switch it to “-(.*)” or “-+(.*)” (one or more dashes) this falls through and the script name gets picked up. Maybe this is another cut and paste artifact.
Thanks for the good work.
Cheers,
Josh
Josh Reed on 11 Apr 2008 at 12:48 am #
Just a quick follow up, the reason why things were failing was because the double dash got converted to an mdash. So it really should be two separate dashes and then the question mark, which makes more sense from a regex standpoint.
Cheers,
Jsoh
tednaleid on 15 Apr 2008 at 3:12 am #
Ack…Thanks Josh. I’ve been having some trouble getting wordpress to not screw up my code.
I believe that I’ve fixed it above, but here’s a pastie link if that causes problem s for anyone.
Armin Heinzer on 16 Apr 2008 at 2:58 pm #
I tried this new version. I could read and create new objects, but I was not able to update an existing object which was read from database before. Such updates were ignored:
b = Book.get(1)
b.name += ‘ updated’
b.save()
I added following lines to the script and now updates works as well:
…
import org.codehaus.groovy.grails.support.*
…
configureHibernateSession()
// added new
appCtx.getBeansOfType(PersistenceContextInterceptor).each { k,v ->
v.init()
}
argsMap[”params”].each { scriptFile ->
executeScript(scriptFile, classLoader)
}
// added new
appCtx.getBeansOfType(PersistenceContextInterceptor).each { k,v ->
v.flush();
v.destroy()
}
…
May be this is not the most elegant solution (I am not a grails expert yet), but at least it fixed my problem.
Armin
along on 27 May 2008 at 10:07 am #
there is something wrong:
includeTargets >> new File ( “${grailsHome}/scripts/Package.groovy” )
includeTargets >> new File ( “${grailsHome}/scripts/Bootstrap.groovy” )
here “>>” should be “<<”
tednaleid on 27 May 2008 at 9:10 pm #
@along
Thanks for the note! It got messed up when I initially posted it and wordpress ate the less than signs, I manually edited it and put the wrong ones in, oops. Should be fixed above now.
Oliver on 30 May 2008 at 6:06 am #
Hi
Excellent work. Thanks for sharing it with us. One question though: In which grails environment is the script running? Development? If so, how is it possible to specify a different environment?
tednaleid on 30 May 2008 at 9:23 am #
@Oliver
By default, scripts run in the development environment. You can run any script in another environment by using the
-Dgrails.env <env_name>parameter.Here’s an example running in the “test” environment (where environments are defined in the grails-app/conf/Config.groovy file)
You can take a look at the environments section of the Grails documentation for more information.
Oliver on 31 May 2008 at 4:59 am #
Thanks. Another question: It is currently not supported to pass arguments to the script because you basically treat all arguments as seperate script files right?
tednaleid on 31 May 2008 at 1:10 pm #
@Oliver, yes, the current script doesn’t pass any arguments to the script and simply treats all parameters as separate script files.
It wouldn’t be that hard to change it to have it instead only parse a single file and pass the parameters along to the calling script.
The call to parseArguments would simply grab the first parameter as the file name and create a variable that contains the remaining arguments.
Then, executeScript would add the remaning parameters as a new item in the Binding object.
Here’s an updated script with these changes (I called it RunScriptWithParms.groovy):
If you have this groovy script (userScripts/showParams.groovy):
And call it with this command line:
You’ll get this as a result with the args passed in:
Steve on 01 Jun 2008 at 3:08 pm #
I tried your code to load my script, but my script wasn’t able to read existing entries in the database (which works when loaded into grails console).
tednaleid on 02 Jun 2008 at 12:05 am #
@Steve Did it give you some sort of error, or were the items just not found? If they weren’t found, I’d be sure that you’re calling the script with the right environment (noted in the comments above). If you’re getting an error, does the stuff mentioned above by Armin Heinzer fix things for you?
Steve on 11 Jun 2008 at 6:31 pm #
Sorry for the delay. I ended up just running my script in the grails console. I’m not familiar enough with grails to try what was suggested by Armin.
Peter-Frank Spierenburg on 08 Dec 2008 at 3:29 pm #
I’m trying to create a script to add new members to my webapp. I’m trying to invoke the MemberController’s save() method to save the new member. The script claims to have created the new member, and I can find the new member using Member.findAll(), but the member’s record doesn’t show up in the database (when I choose production). And when I run the script a second time, Member.findAll() only finds the newly created member, not any of the members I’ve already created.
Peter.
tednaleid on 09 Dec 2008 at 1:47 am #
Peter, that sounds more like a problem with your member save code than the script. Are you sure it saved?
If you open up a “grails console” and paste your script in there and run it, does it get saved in the DB?
Make sure you try to “validate()” before saving to ensure that you have all of the required fields on your domain object. See the grails docs on validation for more details.
Also, make sure that you’re running the script against the environment that you think you’re running it against. You can specify your environment as the first grails parameter:
You can see what environment is currently running by putting this in your script:
If I save that to userScripts/env.groovy and run the above command it prints out “production”.
Peter-Frank Spierenburg on 09 Dec 2008 at 6:46 pm #
Upon further examination, there appears to be some kind of time-delay between statements in the frails shell and SQL winding up in prodDB.log. If I kill the shell too early, no SQL gets logged, and the user is effectively not created. I suspect that what is going on is that the GANT script ends before the database layer has an opportunity to persist the data. I hope I don’t have to add a time delay to my GANT script to account for this. Is there some kind of synchronization method that I can call to ensure that save()d data are actually persisted?
Peter.
tednaleid on 09 Dec 2008 at 9:56 pm #
I’ve never seen that kind of behavior before. You could try putting “flush:true” in your save method to see if that fixes things.
Also, if you’re not letting the script exit normally (but doing some sort of System.exit(0)) that could potentially interrupt things too.