In the project I am working on at the moment, DC, we have a domain model using Hibernate and a set of transfer objects which closely mimic the domain model but are still different to allow changes in domain model to be independent of our REST interface. The codebase contained a lot of code to copy data back and forth between the transfer and domain objects. This is mostly quite trivial code, but there is quite a lot of it and it all needs to be written and tested.
jTransfo is a small library which allows you to handle the conversion between domain and transfer object using annotations on the transfer objects. This way is is possible to have different transfer objects for different scenarios which are related to the same domain object. The project is quite simple. There are no dependencies for the core, but there are modules for spring framework and joda-time integration. While this is not necessary, the library of built to be combined with Lombok to assure that the domain and transfer objects can be quite lean.
Base conversion
For starters, the transfer objects needed to be annotated with the domain object. In DC this was done using string references to prevent circular dependencies between the module with the REST API and the implementation.
@Data
@DomainClass("basepackage.dc.domain.Item")
public class ItemTo {
private Long id;
private String naam;
private String eenheid;
}
Conversion can now be done using something like (DC already uses Spring framework for other things, so we naturally use the Spring integration of jTransfo):
@Autowired
private JTransfo jTransfo;
// create/find object using object finder
Item item = ...;
ItemTO to = jTransfo.convertTO(item, ItemTo.class);
item = jTransfo.convert(to);
// overwrite fields
jTransfo.convert(item, to); // overwrite fields
jTransfo.convert(to, item); // overwrite fields
When the object which needs to be written is not specifically mentioned, then the object finders are used to determine the object which needs to be written. By default, this simply creates an empty object using the no-arguments constructor. More interestingly this can be used to find the base object in the database.
To allow the object finder to know the primary key which needs to be used to find the database object using Hibernate, we changed the transfer object to include extend AbstractIdentifiedTo.
@Data
public abstract class AbstractIdentifiedTo {
@MappedBy(readOnly = true)
private Long id;
}
Note that the id field needs to be read-only as Hibernate does not allow re-writing the id field.
We can now use this in the base class in our example transfer object.
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@DomainClass("basepackage.dc.domain.Item")
public class ItemTo extends AbstractIdentifiedTo {
private String naam;
private String eenheid;
}
We can now build our Hibernate enabled object finder.
@Service
public class HibernateObjectFinder implements ObjectFinder {
@Autowired
private SessionFactory sessionFactory;
@Override
public <T> T getObject(Class<T> domainClass, Object to)
throws JTransfoException {
T res = null;
if (to instanceof AbstractIdentifiedTo) {
Long id = ((AbstractIdentifiedTo) to).getId();
if (null != id) {
res = (T) sessionFactory.getCurrentSession().
get(domainClass, id);
if (null == res) {
throw new JTransfoException("Cannot find object.");
}
}
}
return res;
}
}
This is a spring service. The spring integration module automatically detects the object finders in the application context and adds them to the jTransfo service.
Handling links
Thanks to the object finder, singular links between objects work when converting transfer to domain objects. The linked objects will be retrieved from the database and fields are overwritten.
Now you can check the mapping for all the fields. Fields are by default mapped to fields with the same name in the domain object.
When a field which occurs in the transfer object does not exist in the domain object, you have to annotate it with @NotMapped. Fields which should be copied to the transfer object but not to the domain object should be annotated with @MappedBy(readOnly = true).
When a field has a different name in both objects, or is not directly mapped in the domain object but rather in one of the linked objects, you can use something like
@MappedBy(field = "naam", path = "paragraaf.hoofdstuk", readOnly = true)
private String hoofdstuk;
When the type of the field is different between the transfer and domain object, then a type converter can be used. Type converters can determine their use based on the types of the objects or be referenced either by name or by fully qualified class name.
In DC some fields are represented by enums in the domain object, but in the transfer object these are represented as plain strings. In this case, the type converter can be inferred by type and thus only needs to be registered. As DC uses spring, putting it in the spring context is sufficient. The most practical place to put such declaration is in the META-INF/jTransfoContext.xml file as this is automatically scanned by jTransfo once you include
<import resource="classpath:org/jtransfo/spring/jTransfoContext.xml" />
in your application context.
We include a type converter for all enums in the domain model.
<bean class="org.jtransfo.StringEnumTypeConverter">
<constructor-arg value="basepackage.dc.domain.Geslacht" />
</bean>
For linked objects we also include a special type converter, “readOnlyDomain”. This is provided by jTransfo but still needs to be added to make it available.
<bean class="org.jtransfo.ReadOnlyDomainTypeConverter" />
By default, linked objects will also be converted, copying all fields in it. When converting from a transfer to domain object this may be too relaxed. You often want links between objects to be updated but not the content of the linked object. One solution would be to have a second transfer object in which all fields are read-only. The “readOnlyDomain” type converter is an alternative which does the same.
@MappedBy(typeConverter = "readOnlyDomain")
private BestekTo bestek;
Handling lists of objects
Unfortunately jTransfo cannot determine generic types of objects as these are invisible to reflection (because of type erasure). As a result, you need a type converter for all lists to assure that objects of the right type are included in the list.
In our case, we include two type converters for lists for each type in the domain model.
<bean class="org.jtransfo.ListTypeConverter">
<constructor-arg value="districtToList" />
<constructor-arg value="basepackage.dc.to.DistrictTo" />
</bean>
<bean class="org.jtransfo.ReadOnlyDomainListTypeConverter">
<constructor-arg value="districtToRodList" />
<constructor-arg value="basepackage.dc.to.DistrictTo" />
</bean>
Just declare the type converter to use.
@MappedBy(typeConverter = "vorderingsopdrachtToList")
private List<VorderingsopdrachtTO> vorderingsopdrachten;
Or if you only want the links to be updated but not the fields in the linked objects you can use:
@MappedBy(typeConverter = "vorderingsopdrachtToRodList")
private List<VorderingsopdrachtTO> vorderingsopdrachten;
The list converters also have some options you can set. For example, if the first object is Comparable, using a configuration like the following will automatically sort the list on conversion:
<code><bean class="org.jtransfo.ListTypeConverter">
<constructor-arg value="districtToList" />
<constructor-arg value="basepackage.dc.to.DistrictTo" />
<property name="sortList" value="true" />
</bean>
Assigning IDs in database objects
For DC there was one more problem to fix. In the domain model, there are both objects where the ID is automatically assigned and objects where the ID is first requested by the client and then filled when saving the object.
To handle both cases, the object finder needed to be enhanced. An interface, NeedsIdForCreate was created to mark domain objects which need their id set on object creation. Some care needs to be applied when doing this as Hibernate does not like the id to be set on persisted objects.
public interface NeedsIdForCreate {
/**
* Setter for the database id.
*
* @param id database id
*/
void setId(Long id);
}
public <T> T getObject(Class<T> domainClass, Object to)
throws JTransfoException {
T res = null;
if (to instanceof AbstractIdentifiedTo) {
Long id = ((AbstractIdentifiedTo) to).getId();
if (null != id) {
res = (T) sessionFactory.getCurrentSession().
get(domainClass, id);
if (null == res) {
if (NeedsIdForCreate.class.isAssignableFrom(domainClass)) {
try {
res = domainClass.newInstance();
((NeedsIdForCreate) res).setId(id);
} catch (InstantiationException ie) {
throw new JTransfoException(...);
} catch (IllegalAccessException ie) {
throw new JTransfoException(...);
}
} else {
throw new JTransfoException(...);
}
}
}
}
return res;
}
Conclusion
The conversion was successful. According to sonar, we were able to remove almost 2000 lines of code from the project (almost 10%). More importantly, we lose less time writing boilerplate code and are now more certain that everything is consistent. When in doubt, a quick glance at the transfer object shows what the behaviour is, way easier than looking at the conversion code.