Custom User types in GORM
Recently, I wanted to model a Merchant which like many things in a domain model had an Address. I thought it made sense that the Address was embedded inside the Merchant. Reasons:
- It had no lifecycle outside the Merchant. Merchant dies so should the address.
- It only ever belonged to one and only one Merchant
So pretty obvious this was a composition relationship.
Now, it is possible to model composition relationships in GORM. See here. However, this approach comes with the caveat that the Address must be a GORM object. I didn’t want the Address being a GORM object because GORM objects are powerful in Grails. With all their dynamic finders and GORM APIs they are essentially like really a DAO on steroids. If a developer gets their hands on can do lots of things (not always good things). I didn’t want or need any of this. In addition, a good architecture makes it difficult for developers to make mistakes when they are working under pressure at fast speeds. That means, when you are making design decisions you need to think about the power you need to give, should give and will give.
So with that in mind, I looked into wiring up a custom type for Address. This would just be a data structure that would model the address, could be reused outside the Merchant (thus promoting consistency and again thus promoting good design) and wouldn’t come with the power of the GORM. There is some documentation in the GORM doc’s for custom types but there isn’t a full working example. I had a look at some Hibernate examples and then put managed to put this together and get working.
Here is my address object.
@Immutable class Address { private final String city; private final String country; private final String state; private final String street1; private final String street2; private final String street3; private final String zip; public String getCity() { return city; } public String getCountry() { return country; } public String getZip() { return zip; } public String getState() { return state; } public String getStreet1() { return street1; } public String getStreet2() { return street2; } public String getStreet3() { return street3; } }
Here is my AddressUserType object:
class AddressUserType implements UserType { public int[] sqlTypes() { return [ StringType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType() ] as int[] } public Class getReturnedClass() { return Address.class; } public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException { assert names.length == 7; log.debug(">>mullSafeGet(name=${names}") String city = StringType.INSTANCE.get(rs, names[0], session); // already handles null check String country = StringType.INSTANCE.get(rs, names[1], session ); // already handles null check String state = StringType.INSTANCE.get(rs, names[2], session ); // already handles null check String street1 = StringType.INSTANCE.get(rs, names[3], session ); // already handles null check String street2 = StringType.INSTANCE.get(rs, names[4], session ); // already handles null check String street3 = StringType.INSTANCE.get(rs, names[5], session ); // already handles null check String zip = StringType.INSTANCE.get(rs, names[6], session ); // already handles null check return city == null && v == null ? null : new GAddress(city: city, country: country, state: state, street1: street1, street2: street2, street3: street3, zip: zip); } void nullSafeSet(java.sql.PreparedStatement st, java.lang.Object value, int index, org.hibernate.engine.spi.SessionImplementor session) throws org.hibernate.HibernateException, java.sql.SQLException { if ( value == null ) { StringType.INSTANCE.set( st, null, index ); StringType.INSTANCE.set( st, null, index+1 ); StringType.INSTANCE.set( st, null, index+2 ); StringType.INSTANCE.set( st, null, index+3 ); StringType.INSTANCE.set( st, null, index+4 ); StringType.INSTANCE.set( st, null, index+5 ); StringType.INSTANCE.set( st, null, index+6 ); } else { final Address address = (Address) value; StringType.INSTANCE.set( st, address.getCity(), index,session ); StringType.INSTANCE.set( st, address.getCountry(), index+1,session); StringType.INSTANCE.set( st, address.getState(), index+2,session); StringType.INSTANCE.set( st, address.getStreet1(), index+3,session); StringType.INSTANCE.set( st, address.getStreet2(), index+4,session); StringType.INSTANCE.set( st, address.getStreet3(), index+5,session); StringType.INSTANCE.set( st, address.getZip(), index+6,session); } } @Override public boolean isMutable() { return false; } @Override public boolean equals(Object x, Object y) throws HibernateException { // for now return x.equals(y); } @Override public int hashCode(Object x) throws HibernateException { assert (x != null); return x.hashCode(); } @Override public Object deepCopy(Object value) throws HibernateException { return value; } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } public Class returnedClass() { return Address.class; } }
And here is my Merchant which has an Address.
class Merchant { UUID id; String color; String displayName; //... //... Address address static mapping = { address type: AddressUserType, { column name: "city" column name: "country" column name: "zip" column name: "state" column name: "street1" column name: "street2" column name: "street3" } } }
As stated, with this approach, the Address data structure could be used in other GORM objects. Until the next time take care of yourselves.
Reference: | Custom User types in GORM from our JCG partner Alex Staveley at the Dublin’s Tech Blog blog. |