Partial Bean Cloning and DTOs

From Coder's Log

Jump to: navigation, search

Contents

From The Author

For questions about this article email <<mark at coderslog.com>>.

Introduction

In a multi-tier environment it is always a good practice to isolate access to the datasource in its own tier, even if its only a logical separation. Such separation allows us to create a much more stable and predictable codebase. For example the web layer never tries to access the database and create a transaction therefore, if the data has been retrieved the connections can be reused before the actual web page is generated. This becomes more important when asside from data access we also perform slower access to remote systems.

Why is Lazy-Loading bad

The real problem occurs when we try to access different layers of data. Accessing just the customer information vs. retrieving their order information as well. Before object-relational mapping we solved this problem by executing a stored procedure which limited the amount of data returned aka. sp_get_customer_info vs. sp_get_customer_with_orders. When we made a transition to O/R mapping it became apparent that maintaining trivial SQL statements is not desirable and creates a maintenance havoc in large systems. But how do we determine when to expand which nodes in the O/R heirarchy? Our way comes Lazy-Loading. Lazy-Loading allows us not to specify the depth of the data we want to retrieve but rather allows us to load it on the fly as needed, hence we never access the orders collection under the customer object we never pull the data from the database. The downside of this approach is that you may not access the orders collection until you are well into generating the HTML page and accessing the collection at that point will force a new transaction to be created and the database being accessed. This is not the most desirable outcome in high performance systems.

Aren't DTOs the answer

Many turn to DTOs as a solution by creating a flatter version of the datamodel copying the data from the O/R mapped objects into the DTO and returning it instead of the actual datamodel thus fully separating the data access from the other layers of the system. The biggest flaw with using DTOs for this is that now we have a copy of the datamodel and we need to maintain a sperate procedure for initializing DTOs not to mention we now have to have all sorts of DTOs based on the amount of data we are trying to retrieve. Most seasoned architects will argue that DTOs are overused and have a completely different purpose then what most use them for, and they are right. DTOs were designed to transfer a specific set of data between various components in the system where the underlying datamodel is not acceptable or is too complex or needs to be limited for security reasons. Either way the purpose of DTOs is to alter the structure of the exposed model to other components rather then be the means of transporting data internally within a component.

A Solution

In most of the cases all we would like to accomplish is to create a clone of the datamodel with a limited amount of data, and would prefer to specify that limitation in the layer that is requesting the data not the one doing retrieving. Security asside does the data access layer really care if you are trying to retrieve the customer with or without orders. What we really would like to happen is to have lazy-loading enabled and create a partial clone of the customer object. sometimes accessing the orders collection and other times ignoring it.

One option is to implement the Clone interface how ever it quickly becomes clear that such an approch is too inflexible. It becomes difficult to cover all the possible scenarios especially with deep datamodels.

Another option is to write a method for each type of cloning, how ever maintaining this becomes a bit tedious, but luckaly this approach can be automated using Reflection or BeanUtils.

Utlimatly we want something that would look like

returnObject = BeanCloner.clone(dataConnectedObject,"orders,orders.orderDetails")

in order to retrieve the customer information with orders and details and

returnObject = BeanCloner.clone(dataConnectedObject,"")

to return plain customer information object with the orders collection being null.

The code below will automatically copy all fields of immutable type such as String, Integer, Long. And all other complex types can be specified in the pattern parameter. Wild cards are also allowed for maximum flexebility

To copy all complex properties use

returnObject = BeanCloner.clone(dataConnectedObject,"*")

To copy all child collections use

returnObject = BeanCloner.clone(dataConnectedObject,"**")

All patterns can be nested

returnObject = BeanCloner.clone(dataConnectedObject,"order.**.*")

Source Code

This code is meant to be a refence implementation it is by far not the most efficient, how ever after running some timing it actually has a fairly low overhead in single digit ms range(depending on the depth of the model)


import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;

public class BeanCloner implements Transformer {
	protected static Set<Class> immutableTypes = new HashSet();
	static {
		immutableTypes.add(String.class);
		immutableTypes.add(Integer.class);
		immutableTypes.add(Long.class);
	}

	public static <T> T clone(T object) {
		return clone(object, null, "");
	}
	public static <T> T clone(T object, String pattern) {
		return clone(object, pattern, "");
	}
	public static <T> T clone(T object, String pattern, String path) {
		try {
			if (object != null && (pattern==null || matchPathEnd(path, pattern, object.getClass()))) {
				if (immutableTypes.contains(object.getClass())) {
					return object;
				} else if (Collection.class.isAssignableFrom(object.getClass())) {
					return (T) CollectionUtils.collect((Collection) object, new BeanCloner());
				} else {
					T copy = (T) object.getClass().newInstance();
					Map<String, Object> properties = PropertyUtils.describe(object);
					for (Map.Entry<String, Object> entry : properties.entrySet()) {
						PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(object, entry.getKey());
						if (descriptor.getWriteMethod() != null)
							PropertyUtils.setProperty(copy, entry.getKey(), clone(entry.getValue(), pattern, path + (path.length()>0 ? "." :"") + entry.getKey()));
					}
					return copy;
				}
			} else {
				return null;
			}
		} catch (Exception e) {
			throw new Error(e);
		}
	}
	public Object transform(Object object) {
		return clone(object);
	}
	public static boolean matchPathEnd(String path, String pattern, Class type) {
		if (immutableTypes.contains(type) || path.length()==0)
			return true;
		String[] patterns=pattern.split(",");
		for(String pat:patterns)	{
			String[] pathArray = path.split("\\.");
			String[] patternArray = pat.split("\\.");
			if (patternArray.length >= pathArray.length) {
				String ptrn = patternArray[pathArray.length - 1];
				String pth = pathArray[pathArray.length - 1];
				if (ptrn.equals("**") || ptrn.equals(pth) || (ptrn.equals("*") && !Collection.class.isAssignableFrom(type)))
					return true;
			}
		}
		return false;
	}
}

Download

[Download Source Library]

Personal tools