Announcement Announcement Module
Collapse
No announcement yet.
binding domain objects inside the transactional boundary Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • binding domain objects inside the transactional boundary

    Hi all,

    Following on from a mailing list thread about transactional boundaries (more or less):

    Originally posted by sethladd
    I've been struggling with this issue for a while. Any thoughts? I'd
    love to be totally transactional during read, update, persist but I
    haven't figured out a way to do it *simple*. Any tips would be most
    appreciated.
    I'm still struggling with the same issue, and haven't found any simple answer. In the case of childless domain objects updated on a single form, it's no problem - I simply load the object in one transaction, bind it outside of any transaction, and then persist it in a separate transaction.

    However, when it comes to forms in which a whole hierarchy of child objects is being maintained, this two-transaction solution gets a bit messy, mostly because of the need to touch all lazy-loaded collections when loading, so as to bind properly - especially since in my case binding activates fairly heavy business logic going all over the place, and extending even to domain objects outside of the hierarchy.

    As was mentioned a couple of times, we could simply transactionalize the whole handleRequest, but then our transactional boundary is on the UI layer, which limits reusability (see thread).

    So, let's assume we create a single use-case service object to do the whole thing: that is, loading, binding, and persisting in a single method call which is itself transactionalized.
    Now, if we simply pass the entire HttpRequest into the service object, we're once more tied to a certain UI implementation (servlets).

    If, on the other hand, we try to mirror domain object set... properties on the service object, allow Spring to bind to the service object before the transaction begins, and then bind to the domain objects internally, it quickly gets messy when we're binding to a hierarchy of descendents.

    The third option I thought of was writing a service object which accepts a Map as an input, and passing request.getParameterMap() to this object for binding. This seems less of a dependency than passing in the HttpRequest.

    Here are the beginnings of my solution:
    First, we have a service interface which is referred to by the UI controllers.
    Code:
    /**
     * A service interface for executing complex transactions based on a Map as an input.
     */
    public interface Service {
        /**
         * Execute the transaction on a domain object and its children, friends and relations
         * using a Map of parameters for binding.
         * In the case of a web UI, you could pass in request.getParameterMap().
         * @param identifier the identifier used to load the domain object
         * @param parameters the parameters used to bind the domain object
         * @return an Errors object containing any bind errors if Errors.hasErrors()
         */
        public Errors execute(Object identifier, Map parameters);
    }
    An abstract base class implementing this interface for single use-case services (note that this class duplicates - or rather replaces - some of the functionality performed by the BaseCommandController):
    Code:
    /**
     * Abstract base class for single use-case services, providing a framework for loading, binding and persisting within a single transaction.
     * The {@link #execute(Object,Map) execute(Object,Map)} method should thus be wrapped in a transaction.
     * Clients simply call {@link #execute(Object,Map) execute(Object,Map)} and process the resulting bind errors.
     * Implementations need to override  {@link #serviceBackingObject serviceBackingObject()} in order to provide the appropriate
     * domain object, and  {@link #doExecute(Object) doExecute(Object command)} in order to perform appropriate actions on this
     * domain object.
     */
    public abstract class AbstractService implements Service {
    	public static final String DEFAULT_COMMAND_NAME = "command";
    	private String commandName = DEFAULT_COMMAND_NAME;
    	
    	/*
         * This implementation of execute will get the service backing object through {@link #serviceBackingObject serviceBackingObject()},
         * bind it, and then only call {@link #doExecute(Object) doExecute(Object command)} if no bind errors have occurred.
         */
        public final Errors execute(Object identifier, Map parameters) {
            Object command = serviceBackingObject(identifier);
    		DataBinder binder = bind(parameters, command);
    		if (binder.getErrors().hasErrors())
    		    doExecute(command);
    		return binder.getErrors();
        }
    
        /**
         * The domain object 
         */
        protected abstract Object serviceBackingObject(Object identifier);
        
        /**
         * Perform the actions required by this service object.
         * Only gets called if there are no bind errors.
         */
        protected abstract void doExecute(Object command);
        
        /**
         * Bind the object and return an instance of the binder.
         */
        private final DataBinder bind(Map parameters, Object command) {
            DataBinder binder = new DataBinder(command, getCommandName());
            this.initBinder(binder);
            binder.bind(new MutablePropertyValues(parameters));
            this.doBind(parameters, command, binder.getErrors());
            return binder;
        }
        
        /**
         * Add bind support for application-specific types, such as loading of entities by id,
         * or loading of enumerated types by code.
         * Default implementation is empty.
         * @param binder
         * @throws Exception
         */
    	protected void initBinder(DataBinder binder) {
        }
    
    	
    	/**
    	 * Perform specific bind-support actions.
    	 */
    	protected void doBind(Map parameters, Object command, Errors errors) {
    	}
    	
    	/**
    
    	 * Set the name of the command used in the the bind Errors returned by execute().
    	 */
    	public final void setCommandName(String commandName) {
    		this.commandName = commandName;
    	}
    	/**
    	 * Return the name of the command in the the bind Errors returned by execute().
    	 */
    	public final String getCommandName() {
    		return this.commandName;
    	}
    }
    It is fairly straightforward to write the concrete classes which implement this.
    I've left out Validators for now, but they ought to be easy enough to include (duplicating even more code!).
    The problem comes when we try to fit this into the existing Spring MVC architecture... I like the SimpleFormController, but this involves some hacking, namely:
    Code:
    public class MyForm extends SimpleFormController {
    	private Service someConcreteService;
        
    	public MyForm() {
    		// makes sure the object used to populate the form (the formBackingObject) is not kept in the session
    		setSessionForm(false);
    		// initialize the form from the formBackingObject
    		setBindOnNewForm(true);
    	}
    	
    	protected Map referenceData(HttpServletRequest request) throws ServletException {
    		Map refData = new HashMap();
    		// ... populate reference data
    		return refData;
    	}
    	
    	protected Object formBackingObject(HttpServletRequest request) throws ServletException {
    		// here's a hack for you: only return the object to be bound if it's a new form!
    		// since we want to perform form submission binding within our service object
    		if (this.isFormSubmission(request))
    		    return new Object();
    		SomeDomainObject domainObject = // ... load the domain object from the request
    		return domainObject;
    	}
    
        protected ModelAndView processFormSubmission(HttpServletRequest request,
                HttpServletResponse response, Object command, BindException errors)
                throws Exception {
            String identifier = ... // get identifier from request
            
            Map parameters = request.getParameterMap();
            
            // I'd like keep this as "Errors" rather than BindException, but processFormSubmission wants a BindException
            BindException bindErrors = (BindException) someConcreteService.execute(identifier, parameters);
            
            // Note: we pass in our own bindErrors instead of the empty ones generated by the template method
            return super.processFormSubmission(request, response, command, bindErrors);
        }
    
        public void setSomeConcreteService(
                Service someConcreteService) {
            this.someConcreteService = someConcreteService;
        }
    }
    I've tested the solution and it seems to work...
    So any thoughts about this solution? Have I missed any obvious problems?
    Anybody come up with a more elegant way of encapsulating loading/binding/persistence in a single transaction?

    It would clearly be nicer to have an MVC Controller specifically built for this case, but I'd like to do it without duplicating too much code, which would in all likelihood require some refactoring of the Spring source code.

    Also, I still need to look at the most elegant way of applying some parameter map preprocessing (say, converting parameter names from javascript-friendly names such as parent_childCollection__2__ to Spring DataBinder-friendly names like parent.childCollection[2].
    Last edited by robyn; May 19th, 2006, 05:18 AM.
Working...
X