Grails – Exporting / Importing Domain Objects using DefaultGrailsDomainClass
Lately I’ve been working on a multi-tenant web app that contains a good-size number of domain objects. Within these objects, there exists a sub-set belonging to a root object, and the need arose to be able to quickly duplicate / populate the data within these objects between different instances of the app. It has become quite an interesting problem: certainly one could create some SQL scripts to export/import data, but how does one do it in a user-friendly, Groovy/Grails manner?
The key turned out to be the discovery of the DefaultGrailsDomainClass . This class performs a series of introspections on your domain objects, and allows you to programmatically access quite a bit of information on them. For example, suppose we had the following domain object:
class Pizza{
String name BigDecimal diameter List toppings hasMany = [toppings:Topping]
}
Class ‘Pizza’ contains a name / description, diameter, and a many-to-many relationship with the ‘Topping’ class (although not defined here). Nice and simple… now, the good stuff.
By passing a Domain Object class to DefaultGrailsDomainClass, we can access a good deal of info about that Domain Object. Among other things there are methods to access a Map of the constraints, determine whether or not a property is a relation (1:1,1:m,m:m), and access the properties directly by obtaining a set of GrailsDomainClassProperties . Of particular use to me is the DefaultGrailsDomainClass.getPersistantProperties() method, which returns a GrailsDomainClassProperty List of all the properties that are persisted to the database. That’s pretty excellent.
To illustrate this, let’s get the persistent properties for a Pizza:
def pizzaProperties = new DefaultGrailsDomainClass(Pizza.class).getPersistantProperties()pizzaProperties now contains a list of ‘DefaultGrailsDomainClassProperty’ objects. If we iterate through them, we can see the following information:
[name=diameter,type=double,persistent=true, optional=false, association=false, bidirectional=false, association-type=<null>]
[name=name,type=class java.lang.String,persistent=true, optional=false, association=false, bidirectional=false, association-type=<null>]
[name=toppings,type=interface java.util.List, persistent=true, optional=true, association=true, bidirectional=false, association-type=one-to-many]
Think of the possibilities of what can be accomplished by programmatically accessing the properties on your domain object! However, I am not exceedingly clever:
good deal of info about each property.
else{//do something clever!}
}
}
The exportClassSingle method above accepts the ConfigObject which will be populated with the domain object’s data, the name of the class, and an instance of the domain object. It is relatively simple: it takes the class, loads its persistent properties, loops through through them and if the property is not an association, it is added to the config object. By repeatedly calling the method for each of our Domain objects, we build up the ConfigObject which contains class and instance data. The preExportClean() method looks for and escapes any usage of ‘$’, which can cause some odd behavior if not escaped. We ignore associations here because in the real app, everything is connected to one root object; if there were associations among the child objects that we wished to capture then the above method would be quite different.
The resulting ConfigObject can then be converted into a String containing a DSL describing your objects and data. The string can then be loaded back into a ConfigObject using the ConfigSlurper.
Given the following DSL:
pizza{
pizza_0{
name="Small"
diameter="8.0"
}
pizza_1{
name="Medium"
diameter="14.5"
}
pizza_2{
name="Large"
diameter="21.0"
}
}
and assuming it’s saved in a String called ‘pizzaDSL’, we can import this data into our system using the following steps:
1. Import the DSL using ConfigSlurper.
ConfigObject config = new ConfigSlurper().parse( pizzaDSL )
The ConfigObject config now contains a map of ConfigObjects. Using the key of ‘pizza’ returns a map of 3 more ConfigObjects.
2. Construct objects using the configObject.
config["pizza"].each{key,map->
Pizza p = new Pizza()
p.properties = map
//set toppings
p.save()
}
One could also iterate through the root config object (in the case where we had more objects, e.g. ‘topping’) and use the key to determine which object to create and populate. This simple case has only one class, Pizza (and thus only one key).
3. Celebrate
Well, that’s it. I hope this helps explain the usage of the Default Domain Class and that you all find a usage for it.
