5 Replies Latest reply on May 20, 2011 9:40 AM by Adam Warski

    a case study problem

    Mehdi Zehtaban Newbie

      I have a conceptual problem with envers mechanism.

       

      Untitled-1.png
      Please follow these senario :

      I have a UI page for customer data entry. Every customer has it's own properties and a person object and a collection of SalesDivision. I want to have a feature in customer UI to let the user to see versions of customer and select a version and see the data at the selected version.

       

      I think I should show a list of revisions for customer to user. with auditReader.getRevisions(Customer.class, custoemrId);

      But there is some issue to do this.

       

      1- First of all that I add a new customer every thing sounds good. Because a person and a customer will be inserted in a revision.

      2- I edit just person property so just the person will audited in person_aud and when I use auditReader.getRevisions(Customer.class, custoemrId); it doesn't show a change on person.

       

      If I show a list of revisions from person to user with auditReader.getRevisions(Person.class, customerPersonId) then

      if I change the customer because collection of customers doesn't changed, then person wont be audited.

       

      So what can I do?

        • 1. a case study problem
          Hernán Chanfreau Master

          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
            Mehdi Zehtaban Newbie

            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 :

            Untitled-1.png

            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
              Hernán Chanfreau Master

              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
                Mehdi Zehtaban Newbie

                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
                  Adam Warski Master

                  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