-
1. a case study problem
hernanbolido May 15, 2011 5:34 PM (in response to mehdizehtaban)Hi!
I think there are 2 options:
- Create a query for both entities revisions and "merge" the revisions.
- Make "dirty" the parent when you change a child. Here you would be forcing a change on the parent entity.
There is a similar issue using hibernate optimistic concurrency strategy with parent - child relations, when you must warranty concurrency on the parent entity.
Maybe Adam or someone else has other ideas?
Hope it will help.
Regards. Hernán
-
2. a case study problem
mehdizehtaban May 16, 2011 2:12 AM (in response to hernanbolido)Dear Hernán
Thanks for your reply. Indeed I believe we should support 2 kind of association in a system : Composition and Aggregation.
Let's take a look at this :
In this system, we have some composition and an aggregation relation.
I made a new MyAuditListener that extends default AuditEventListener.
Also I made a new annotation called AuditParentOnChange.
In the update of new MyAuditListener if a child like Customer changed I check look for @AuditParentOnChange annotation on the relation defined in the Customer class. So if I found the annotation I make a new revision for the parent too.
But in the Order class, I wont use @AuditParentOnChange. So whenevere an order changed, no new version will make for the Customer.
What do you think about this logic?
If you are agree with me, there is some other problem. As you can see in the diagram SalesDivision has a composition relation with it's parent Customer class. In my logic the we should annotate the relation of SalesDivision and Customer with @AuditParentOnChange. So whenever a SalesDivision changed, a new version of customer will be stored at customer_aud.
Now the question is : should I make a new version for Person in this transaction?
-
3. a case study problem
hernanbolido May 16, 2011 8:24 AM (in response to mehdizehtaban)Hi Mehdi!
Reading your post an issue came to my mind... Hibernate make no differences between associations and compositions (on persistent entities behaviors, you must keep this on your code). So, you must either cast your objects on your revision listener or the relation is bidirectional. Am I wright? (thinking about how to generalize your solution)
Regards. Hernán.
-
4. Re: a case study problem
mehdizehtaban May 17, 2011 2:41 AM (in response to hernanbolido)Dear Hernán
As you know we should pay some cost to have auditing. In my previous suggestion we should pay more cost during normal operations of users. (Insert/Update/Delete). I decide to change my mind. So I prefer to pay this cost at viewing the history of an entity that may occure less than transactions.
So I wrote a utility class to gather all changes of the given entity based on the entity relations. The aproach is search for all @ManyToOne and @OneToMany relations with the given entity.
The utility class returns a list of RevisionEntity contains all changes of parent of entity and the child of the given entity. I should think about this approach more and test it in some other usecases. But for start, this solution solved my problem. What do you think about it?
Here is my utility class code. I know we change imporve this class in a better way, but this is the first version.
The method we nee is getChangesForEntity.
Whith this approach whenever a Person updated or a SalesDivision updated and I call getChangesForEntity(customer) I have revisions for perosn updates and SalesDivisionUpdates too.
/**
* @author Mehdi Zehtaban
* Date: May 16, 2011
* Time: 12:00:59 PM
*/
public class EnversEntityHistoryUtil {
protected AuditReader auditReader;
public EnversEntityHistoryUtil() {
this.auditReader = SpringUtils.getAuditReader();
}
public List<RevisionEntity> getChangesForEntity(Object entity) {
Set<Number> numbers = detectChangesOnEntity(entity, true, true);
Number n = new Long(0);
numbers.remove(n);
return sortRevisions(numbers);
}
protected Set<Number> detectChangesOnEntity(Object entity, boolean detectParentChanges, boolean detectChildrenChanges) {
if (entity instanceof HibernateProxy){
return null;
}
Set<Number> result = new LinkedHashSet<Number>();
try {
Object entityId = getEntityId(entity);
List<Number> numberList = this.auditReader.getRevisions(entity.getClass(), entityId);
result.addAll(numberList);
//detects MayToOne annotations.
Class<? extends Object> aClass = entity.getClass();
Method[] methods = aClass.getMethods();
for (Method method : methods) {
if (detectParentChanges && method.getAnnotation(ManyToOne.class) != null) {
Object parent = method.invoke(entity);
List<Number> parentRevisions = getEntityRevisions(parent);
result.addAll(parentRevisions);
Set<Number> numberSet = detectChangesOnEntity(parent, true, false);
if (numberSet != null && !numberSet.isEmpty()) {
result.addAll(numberSet);
}
} else if (detectChildrenChanges && method.getAnnotation(OneToMany.class) != null) {
List children = (List) method.invoke(entity);
for (Object child : children) {
List<Number> entityRevisions = getEntityRevisions(child);
result.addAll(entityRevisions);
Set<Number> numberSet = detectChangesOnEntity(child, false, true);
if (numberSet != null && !numberSet.isEmpty()) {
result.addAll(numberSet);
}
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return result;
}
private List<Number> getEntityRevisions(Object entity) throws IllegalAccessException, InvocationTargetException {
Object parentEntityId = null;
Class parentClass = null;
if (entity instanceof HibernateProxy) {
HibernateProxy proxy = (HibernateProxy) entity;
Serializable serializable = proxy.getHibernateLazyInitializer().getIdentifier();
System.out.println("serializable = " + serializable);
parentEntityId = serializable;
parentClass = proxy.getHibernateLazyInitializer().getPersistentClass();
} else {
parentEntityId = getEntityId(entity);
parentClass = entity.getClass();
}
List<Number> parentRevisions = auditReader.getRevisions(parentClass, parentEntityId);
return parentRevisions;
}
protected List<RevisionEntity> sortRevisions(Set<Number> revisions) {
if (Empty.isEmpty(revisions)) {
return null;
}
EntityManager em = SpringUtils.getEntityManager();
StringBuffer hql = new StringBuffer("select o from com.foursun.cymap.common.data.to.RevisionEntity o where o.id in (");
int i = 0;
if (Empty.isNotEmpty(revisions)) {
for (Number revision : revisions) {
if (i > 0) {
hql.append(",");
}
hql.append(revision);
++i;
}
}
hql.append(") order by o.timestamp asc");
Query query = em.createQuery(hql.toString());
return query.getResultList();
}
/**
* returns primary key of the given entity.
*
* @param entity
* @return
*/
protected Object getEntityId(Object entity) throws InvocationTargetException, IllegalAccessException {
if (entity instanceof HibernateProxy) {
HibernateProxy proxy = (HibernateProxy) entity;
return proxy.getHibernateLazyInitializer().getIdentifier();
} else {
Class<? extends Object> aClass = entity.getClass();
Method[] methods = aClass.getMethods();
for (Method method : methods) {
if (method.getAnnotation(javax.persistence.Id.class) != null) {
return method.invoke(entity);
}
}
}
return null;
}
}
-
5. a case study problem
adamw May 20, 2011 9:40 AM (in response to mehdizehtaban)Hello,
your problem is a well-known one . And both solutions have their ups and downs (btw. as for the question in the last-but-one post - I think that in the case you describe a new audit entry for Parent should also be created). Ideally this should be done with a query, but I'm not sure it's possible.
I like your utility class a lot - maybe you could put it on github and/or write about it on a blog, so that I could put a link to it in the Envers FAQ?
Adam