AOP Data Access and Hibernate Transactions
Hi All:
Have not found a thread that addresses this issue, so hopefully it has not been covered before. I am trying to write a generic Auditer class that will track changes to objects made through the service layer. The Auditer class is applied as an aspect against certain service layer calls. Specifically, the joinpoint is set to fire on execution of service methods matching addFooEntity, updateFooEntity, deleteFooEntity and the Auditer class supplies the appropriate advice (e.g., log something like "User JoeBlow added FooEntity with Id 123"). This all works fine, except in the case of an update, where I want to track which fields change. For an update, I would like to use Before advice to get the existing object from the database before the update and compare it against the updated entity.
Here is the relevant code from the Auditer class:
Code:
@Before("com.xonos.common.aop.Auditer.editActionPointcut()")
public void doEditAudit(JoinPoint jp) throws Throwable {
Object service = jp.getThis();
Object updatedEntity = jp.getArgs()[0];
Method serviceGetMethod = service.getClass().getMethod("get"+updatedEntity.getClass().getSimpleName(), new Class[]{java.lang.Long.class});
Method entityIdGetter = updatedEntity.getClass().getMethod("getId");
Long id = (Long)entityIdGetter.invoke(updatedEntity);
// get existing entity
Object existingEntity = serviceGetMethod.invoke(service, id);
existingEntity.getClass().getMethod("getId").invoke(existingEntity);
Map existingEntityPropMap = BeanUtils.describe(existingEntity);
Map updatedEntityPropMap = BeanUtils.describe(updatedEntity);
// at this point, just write the two objects' properties to STDOUT
for(Iterator i = existingEntityPropMap.keySet().iterator(); i.hasNext();){
Object x = i.next();
System.out.println(x+" : "+existingEntityPropMap.get(x));
}
for(Iterator q = updatedEntityPropMap.keySet().iterator(); q.hasNext();){
Object y = q.next();
System.out.println(y+" : "+updatedEntityPropMap.get(y));
}
}
Here's my problem: everything works OK, but the existingEntity always shows up as the same as the updatedEntity. After looking closely at the logs, this is what I see: 1) the Hibernate transaction begins and the updated entity is placed into the session, 2) my AOP Auditer class fires and tries to retrieve the existing object 3) Hibernate sees that the object exists in session and returns the object with updated information, never hitting the database. I have tried setting the precedence of the Auditor class higher than the Hibernate transaction proxy using the order attribute in my config, but this didn't help. There are probably some kludgy ways to get around this issue programmatically, but want to see if anyone has an idea for elegantly solving this problem declaratively.
Thanks in advance for any ideas,
Dave
Look to the Strategy pattern, young Skywalker...
First, let me say that I find it interesting that someone else was having this problem almost simultaneously! :D
So, for some quick backstory... my need is the ability to generate a delta from the state differences between the entity in the database and the entity to-be-persisted. We have an external system (PeopleSoft) that requires updates of particular value changes on properties of persitent domain objects, and it has no knowledge of our application's domain model. So, what I need to ship to the external system is a collection of property names and the associated before-and-after data values for each property. Sounds pretty simple, right?
...exactly. Man, did I bang against this problem for a long time! At first, like you, I thought, "hey, I'll just write a little delta generator aspect class and wrap it around the transactional service methods I care about (create/save/delete) and it'll be done."
Wrong! The ApplicationContext / HibernateTransactionManager didn't like an Aspect being wired with a reference to the SessionFactory it was managing that was not, itself, being managed by the HibernateTransactionManager. Consequently, my ApplicationContext would blow up and never initialize. My apologies for not providing the specific Exception trace here from that nightmarish expedition.
Next attempt: a class that implemented the Hibernate Interceptor interface. I had a kind of eureka moment when I remembered / realized that Hibernate has an event architecture, and that it tracks all the phases of the Session lifecycle at a very high level of granularity. Anyway, I wrote a DeltaGeneratorInterceptor that I then wired into the SessionFactory bean definition as follows, for per-Session interception:
Code:
<bean id="deltaGeneratorInterceptor"
class="x.y.z.DeltaGeneratorInterceptor"/>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
<property name="entityInterceptorBeanName"
value="deltaGeneratorInterceptor"/>
</bean>
Sadly, this also didn't work. In the DeltaGenerator#onFlushDirty method, the 'previousState' argument was alway null, and therefore I didn't have the old state values against which to compare the new, transient, about-to-be-persisted values.
At this point (yesterday), I stepped back and totally reevaluated my requirement and concerns:
- Entity is passed to transactional service method (create/save/delete)
- Pull the persistent 'old' state from the database prior to creating/saving/deleting the transient, 'current' state
- Compare both states, and produce a delta representation (aka Map)
- Allow the create/save/delete to occur
- Done!
I realized that my solution had to be involved or 'inside' the transactional context somehow, because my attempts to wrap around or intercept the transactional method execution in a loosely decoupled way were not working. This is when I had my major eureka moment: the friggin' Strategy pattern, man!
So, I have an interface, DeltaStrategy, that handles the save/delete on behalf of the Service:
Code:
public interface DeltaStrategy<V> {
public V saveEntity(V entity);
public void deleteEntity(V entity);
public void generateDelta(V previousEntityState, V currentEntityState);
}
Next, I have another interface, DeltaStrategizable, that will be implemented by objects using a DeltaStrategy object (this may be optional):
Code:
public interface DeltaStrategizable<V> {
public void setDeltaStrategy(DeltaStrategy<V> deltaStrategy);
}
Then, I update my Service interface like so:
Code:
public interface PersonService extends DeltaStrategizable<Person> {
...
}
Here's the config XML for that:
Code:
<bean id="personDeltaStrategy" class="x.y.z.PersonDeltaStrategy">
<property name="sessionFactory" ref="sessionFactory"/>
<property name="personRepository" ref="personRepository"/>
</bean>
<bean id="personService" class="x.y.z.service.PersonServiceDefaultImpl">
<constructor-arg ref="personRepository"/>
<constructor-arg ref="personDeltaStrategy"/>
</bean>
So now, when PersonService.savePerson(Person p) is called, it delegates the actual save to its injected DeltaStrategy. Inside the DeltaStrategy object, I retrieve the 'old' entity state by calling personRepository.findPersonById(person.getId()). The important thing to do right after retrieving the 'old' entity is to evict it from the Session, because otherwise you'll get a NonUniqueObjectException from having 2 instances of the same Session-managed entity in the scope of a single Session. Once you do that, you'll have a detached entity that contains the 'old' state and the transient 'current' entity that can then be handed off to the Repository for the save/update/delete.
Now, you can go ahead with generating a delta, writing to an audit log, etc!!
HTH