-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 8 posts ] 
Author Message
 Post subject: Business Rules
PostPosted: Tue Oct 28, 2003 9:12 am 
Newbie

Joined: Tue Oct 28, 2003 9:07 am
Posts: 1
Take a look at the following scenario:

If a "Order" persistence class is associate to a "Customer" persistence class and the business rules is:
We can not make a new Order if the Customer associated with that Order has some credit restrictions.

Where is the right place to put this rule is Hibernate ?
At a Interceptor class ? At the Order class (using the validate() method of the Validatable interface) ?

Miguel.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Oct 28, 2003 10:48 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 3:00 pm
Posts: 1816
Location: Austin, TX
You pretty much have two options.
1) Perform the validations up-front in your application code.
2) Use an Interceptor impl to perform the validations.

There are some caveats to the second approach. The normal thought process would be to perform the validations during the onSave(), onFlushDirty(), etc callbacks. But the validations cannot be setup that way because it is illegal to interact with the session in any way (and yes this includes triggering lazy initialization) during these callbacks. Doing so causes very weird side-effects.

The only viable implementation I have come up with for the second approach was to "schedule" validations during the onSave(), onFlushDirty(), etc callbacks. Then actually performing the validations during the postFlush() callback.

Here is a snippet of my Interceptor which sets this up...
Code:
public final class HibernateEventSink
implements Interceptor, Serializable
{
    private static Logger log = Logger.getLogger(HibernateEventSink.class);
    private List inserts = new ArrayList();
    private List deletes = new ArrayList();
    private Map updates = new Hashtable();

    private UserResolver userResolutionStrategy;
    private EventHub eventHub;
    private ValidationGateway validationGateway;

    public HibernateEventSink()
    {
        super();
        log.info("Event sink generated");
    }

    public boolean onSave(
                Object entity,
                Serializable id,
                Object[] state,
                String[] propertyNames,
                Type[] types )
    {
        log.info( "Entity being saved [" + entity + "]" );
        if (entity instanceof DomainEntity)
        {
            if ( entity instanceof AuditableEntity )
            {
                String user = getUser();
                AuditInfo auditInfo = ((AuditableEntity)entity).getAuditInfo();
                Date now = new Date();
                log.info( "Creation made by user [" + user + "] at [" + now + "]" );
                auditInfo.setCreatedDate( now );
                auditInfo.setCreatorId( user );
                auditInfo.setModifiedDate( now );
                auditInfo.setModifierId( user );
            }
            // Schedule the insert for validation and event propogation
            inserts.add(entity);
        }
        return false;
    }

    public boolean onFlushDirty(
                Object entity,
                Serializable id,
                Object[] currentState,
                Object[] previousState,
                String[] propertyNames,
                Type[] types )
    {
        log.info( "Entity being flushed-dirty [" + entity + "]" );
        if (entity instanceof DomainEntity)
        {
            DeltaSet changes = DeltaSetCalculator.calculateDeltaSet( propertyNames, previousState, currentState );

            if ( entity instanceof AuditableEntity )
            {
                String user = getUser();
                Date now = new Date();
                AuditInfo auditInfo = ((AuditableEntity)entity).getAuditInfo();
                log.info( "Changes made by user [" + user + "] at [" + now + "]" );
                auditInfo.setModifiedDate( now );
                auditInfo.setModifierId( user );
            }
            // Schedule the update for validation and event propogation
            updates.put(entity,changes);
        }
        return false;
    }


    public void onDelete(
                Object entity,
                Serializable id,
                Object[] state,
                String[] propertyNames,
                Type[] types )
    {
        log.info( "Entity being deleted [" + entity + "]" );
        if (entity instanceof DomainEntity)
        {
            // Schedule the delete for validation and event propogation
            deletes.add(entity);
        }
    }

    public void postFlush( java.util.Iterator entities )
    {
        log.info( "Starting postFlush" );
        try
        {
            String user = getUser();
            log.debug("Handling inserts");
            for (Iterator insertItr = inserts.iterator(); insertItr.hasNext(); )
            {
                final DomainEntity entity = (DomainEntity)insertItr.next();
                log.info("Starting postFlush (insert) handling for [" + entity + "]");

                validationGateway.validateCreation(entity);
                log.debug( "Validations succeeded" );

                eventHub.handleCreation(entity, user);
                log.debug( "Event hub notifications succeeded" );
            }

            log.debug("Handling updates");
            for (Iterator updateItr = updates.entrySet().iterator(); updateItr.hasNext(); )
            {
                final Map.Entry entry = (Map.Entry)updateItr.next();
                final DomainEntity entity = (DomainEntity)entry.getKey();
                final DeltaSet changes = (DeltaSet)entry.getValue();
                log.info("Starting postFlush (update) handling for [" + entity + "]");

                validationGateway.validateModification(entity, changes);
                log.debug( "Validations succeeded" );

                eventHub.handleModification(entity, changes, user);
                log.debug( "Event hub notifications succeeded" );
            }

            log.debug("Handling all deletes");
            for (Iterator deleteItr = deletes.iterator(); deleteItr.hasNext(); )
            {
                final DomainEntity entity = (DomainEntity)deleteItr.next();
                log.info("Starting postFlush (delete) handling for [" + entity + "]");
                eventHub.handleDeletion(entity, user);
            }
        }
        catch(ValidationException e)
        {
            throw new ValidationExceptionRuntimeWrapper(e);
        }
        finally
        {
            inserts.clear();
            updates.clear();
            deletes.clear();
            log.info( "Completed postFlush" );
        }
    }

    ...


Top
 Profile  
 
 Post subject:
PostPosted: Thu Dec 04, 2003 3:28 am 
Pro
Pro

Joined: Tue Aug 26, 2003 8:07 pm
Posts: 229
Location: Brisbane, Australia
Myself, I'd do this with domain logic, cause I like Object Orientation so much (sorry, I tried not to say that, but I couldn't resist) :)

I'm assuming that your Customer/Order relationship is mapped as a fairly standard Parent/Child association?
I'm also assuming that Customer and Order are in the same java package, which might not be appropriate for you, but whatever.

I'd disallow everyone else in the system from creating orders by making the Order setter package protected. Then create an "addOrder(Order)" method on the Customer class and do your credit checking in the addOrder() method.

Hibernate Team, I'm looking for a bit of validation on this technique.
It seems like a better way of implmenting business rules to me, but I might be missing something obvious?

_________________
Cheers,
Shorn.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Dec 05, 2003 2:01 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 7:19 pm
Posts: 2364
Location: Brisbane, Australia
I also prefer the Rules in the domain object. Maybe its because we are from the same town. In anycase, its a preference / style issue.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 06, 2004 7:31 am 
Newbie

Joined: Thu Dec 04, 2003 5:53 am
Posts: 11
Location: Toulouse, France
If I understand the HibernateEventSink correctly, the idea is that after the flush, any validation errors will throw a runtime exception, hence causing transactions to be rolled back.

This is fine for validation. But consider the case where you want to insert new rows as a result of modifications in another table.

Concretely, I would like to create a Message for another user when the status of an Entity owned by him has been changed.

I could of course create the message on the Entity's setStatus() method, and this may be the best course. (comments?)

However, it would create two messages in the unlikely occurrence that the Entity's status was set from x to y and then back to x again before being persisted.

Ideally I would like to create the message only when actually persisting the changes. But I cannot do this in Interceptor.onFlushDirty() because I musn't access the session to create the new messages. And if I do it in postFlush, using the HibernateEventSink, my new messages will never get flushed to the database, unless I'm missing something.

Any ideas?
Thanks in advance,
Assaf


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 06, 2004 8:37 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 7:19 pm
Posts: 2364
Location: Brisbane, Australia
I always drive business rules/processes at a higher level then the persistence level. Obviously, where reasonable in the steps involved in the processing is conducted within a transaction so the complete update can be rolled back if necessary. In my view the modifcation is made the message is sent, then transaction is over. If further modifications are made then you will send another message unless the business rules also look for a recent (definition is necessary here) message was sent. This is-to/would avoid multiple moddification messages going out to the interested parties. Trying to anymore then relatively straight forward work in an interceptor is a code smell. Hard to test, debug and maintain.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 12, 2004 8:07 am 
Newbie

Joined: Thu Dec 04, 2003 5:53 am
Posts: 11
Location: Toulouse, France
I finally settled on another solution, which enforces the validation of certain entities prior to persisting them but allows me to avoid validation in the entity's set methods themselves. Using this method, the business logic is still taken care of prior to reaching the interceptor, and I can perform more complicated validation/action than in the set methods, such as validation which relies on graphs of sub-objects or on the interrelationships of various properties.

This is done using the Validatable interface:
Code:
/**
Used by classes for which the user has to perform some action which sets them to
"valid" prior to saving.
*/
public interface Validatable {
   /** check if the class is validated */
   public boolean isValidated();
}

And the following Interceptor:
Code:
/**
An interceptor for checking that entities are validated before saving/updating.
*/
public class ValidatingInterceptor implements Interceptor, Serializable {
   
   public ValidatingInterceptor () {}
   
   public Object instantiate(Class clazz, Serializable id) {
      return null;
   }
      
                   
   public void onDelete(Object entity,
                         Serializable id,
                         Object[] state,
                         String[] propertyNames,
                         Type[] types)
      throws CallbackException
   {
   }

   // called when the object is updated in the database
   public boolean onFlushDirty(Object entity,
                                Serializable id,
                                Object[] currentState,
                                Object[] previousState,
                                String[] propertyNames,
                                Type[] types)
      throws CallbackException
   {
      if (entity instanceof Validatable) {
         Validatable validatable = (Validatable) entity;
         if (!validatable.isValidated())
            throw new CallbackException
               ("Validatable entity has not been validated!");
      }
        return false;
   }

   public boolean onLoad(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types)
      throws CallbackException
   {
      return false;
   }

   public boolean onSave(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types)
      throws CallbackException
   {
      if (entity instanceof Validatable) {
         Validatable validatable = (Validatable) entity;
         if (!validatable.isValidated())
            throw new CallbackException
               ("Validatable entity has not been validated!");
      }
      return false;
   }

   public void postFlush(Iterator entities)
      throws CallbackException
   {
   }

   public void preFlush(Iterator entities)
      throws CallbackException
   {
   }   
   
   public Boolean isUnsaved(Object entity) { return null; }
   
   public int[] findDirty(Object entity,
                       Serializable id,
                       Object[] currentState,
                       Object[] previousState,
                       String[] propertyNames,
                       Type[] types)
   {
      return null;
   }
}

Your entity then looks something like this:

Code:
public class Entity implements Validatable {
   private boolean m_validated = false;

   // ...

   public void validate() throws MyException {
      if (!m_validated) {
         // ... perform validation and other actions
         // (like sending messages in my case),
         // and throw MyException if necessary
         m_validated = true;   
      }
   }

   public boolean isValidated() {
      return m_validated
   }
}


Note that client is forced to call the validate() method before committing the session (otherwise an exception is thrown).

Here are the caveats: when deleting an object, you first need to set it to validated, otherwise flushDirty will throw an error.

- Assaf


Top
 Profile  
 
 Post subject: Re: Business Rules
PostPosted: Sun Aug 14, 2011 4:27 pm 
Newbie

Joined: Wed Jun 08, 2011 6:28 pm
Posts: 5
Classic problem, and it's been unbelievably arcane and awkward to solve.

Now there's this:

Code:
Constraint: balance < creditLimit

balance = sum(orders.total where paid = false)

order.total = sum(lineitems.amount)

lineitem.amount = quantity * partPrice

lineitem.partPrice = copy(part.price)


This is executable - it plugs into Hibernate / JPA events, and is enforced for all transactions that update the relevant Domain Objects (delete an order, associate line item to different part, change a quantity, etc etc).


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 8 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.