PDA

View Full Version : History tables and error logging



turgayz
Aug 27th, 2004, 07:32 AM
Hi,
[I hope this is the right forum]
I need to track all updates to the database in my application. I mean, all versions of a record must be kept.
For example, in my DAO, if I have a method like


public User update_updateUser(User user) {
getHibernateTemplate().saveOrUpdate(user);
}


I want to make this run as:


public User update_updateUser(User user) {
BackupUser backupUser=new BackupUser(User);
getHibernateTemplate().save(backupUser);
getHibernateTemplate().saveOrUpdate(user);
}


For each entity, I will need to create another entity (Like BackupUser), which maps to another database table (Like BACKUP_USER).

I would like to learn if it is possible, by use of AOP, to have the functionality I mentioned above - without handcoding manually for each Hibernate call.

Another similar requirement is error logging. Whenever Spring catches an Exception, I would like to run this code:


ApplicationErrorLog appErrorLog = new(ApplicationErrorLog);
getHibernateTemplate().save(applicationErrorLog );


I know that I have to read a lot about AOP, but until then, your help, simple code examples and appcontext.xml will be will be very much appreciated.

Regards,
Turgay Zengin

irbouho
Aug 27th, 2004, 10:26 AM
did you consider using database triggers?

turgayz
Aug 27th, 2004, 10:42 AM
Right, the natural way to implement this is using an update trigger, but I do not want to go into database triggers, because I want the database to be portable.

Regards,
Turgay Zengin

turgayz
Sep 1st, 2004, 05:11 AM
This is what I could do, after studying AOP:

SaveDatabaseInterceptor is for catching save operations, and then saving a backup object to the database. It works if the object implements Auditable --if(args0 instanceof Auditable)--, and if this is not a "insert into" but a "update" operation --if(oldId!=null)--.



public class SaveDatabaseInterceptor implements MethodInterceptor {
private SystemDAO systemDAO = null;

public void setSystemDAO(SystemDAO systemDao) {
this.systemDAO = systemDao;
}

public Object invoke(MethodInvocation invocation) throws Throwable {
Object[] args=invocation.getArguments();
Object args0=args[0];
if(args0 instanceof Auditable) {
BasePersistentObject bpo=(BasePersistentObject)args[0];
Long oldId=bpo.getId();
if(oldId!=null){
BasePersistentObject backupBPO=bpo.getBackupObject();
systemDAO.saveBPO(backupBPO);
System.out.println("save advice worked!:" + invocation);
}
}
Object rval = invocation.proceed();
return rval;
}
}


Each Auditable entity class defines a method to get a "Backup" object. For example, in User.java:



public BasePersistentObject getBackupObject() {
return new Backup_User(this);
}


Backup_User is a seperate class, which also has a mapping file for hibernate.

For "delete" operations, I have the following.



public class RemoveDatabaseInterceptor implements MethodInterceptor {
private SystemDAO systemDAO = null;

public void setSystemDAO(SystemDAO systemDao) {
this.systemDAO = systemDao;
}

public Object invoke(MethodInvocation invocation) throws Throwable {
Object[] args=invocation.getArguments();
Object args0=args[0];
if(args0 instanceof Auditable) {
BasePersistentObject bpo=(BasePersistentObject)args[0];

if(bpo!=null){
BasePersistentObject deletedBPO=bpo.getDeletedObject();
systemDAO.saveBPO(deletedBPO);
System.out.println("remove advice worked!:" + invocation);
}
}

Object rval = invocation.proceed();
return rval;

}

}


Likewise, each Auditable class has a method to get a "Deleted" object:


public BasePersistentObject getDeletedObject() {
return new Deleted_User(this);
}

Relevant parts from the applicationContext.xml:


<bean id="DAOTemplate" lazy-init="true"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames">
<list>
<value>saveDatabaseAdvisor</value>
<value>removeDatabaseAdvisor</value>
</list>
</property>
</bean>

<bean id="userDAO" parent="DAOTemplate" >
<property name="proxyInterfaces"><value>dao.UserDAO</value></property>
<property name="target"><ref local="userDAOTarget"/></property>
</bean>

<bean id="userDAOTarget" class="dao.hibernate.UserDAOHibernate">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>

<bean id="systemDAO" parent="DAOTemplate" >
<property name="proxyInterfaces"><value>dao.SystemDAO</value></property>
<property name="target"><ref local="systemDAOTarget"/></property>
</bean>

<bean id="systemDAOTarget" class="dao.hibernate.SystemDAOHibernate">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>

<bean id="saveDatabaseInterceptor" class="util.SaveDatabaseInterceptor">
<property name="systemDAO"><ref local="systemDAO"/></property>
</bean>

<bean id="saveDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointc utAdvisor">
<property name="advice"><ref local="saveDatabaseInterceptor"/></property>
<property name="patterns">
<list>
<value>.*save.*</value>
</list>
</property>
</bean>

<bean id="removeDatabaseInterceptor" class="util.RemoveDatabaseInterceptor" >
<property name="systemDAO"><ref local="systemDAO"/></property>
</bean>

<bean id="removeDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointc utAdvisor">
<property name="advice"><ref local="removeDatabaseInterceptor"/></property>
<property name="patterns">
<list>
<value>.*remove.*</value>
</list>
</property>
</bean>


I now have only one problem with the remove operation. When the entity itself is deleted from the master table(User), a record will be inserted into the "Deleted_" table (Deleted_User). But I cannot put a foreign key to the "Deleted" table to point to the "deletedUser", because the record will be deleted from the master table...

I am thinking about not deleting from the master table actually, but marking it as deleted, using a field called DATABASE_STATUS, and setting it to "DELETED". ("ACTIVE" if not deleted). But this also introduces another problem: If I have unique constraints on the table (USERNAME UNIQUE), than I cannot insert a new record violating that unique constraint. Any ideas?

Regards,
Turgay Zengin

turgayz
Oct 12th, 2004, 11:55 AM
Here is the design I came up with, hoping that it could be useful for others as well. Any comments, critiques, improvement suggestions are welcome.

SaveDatabaseInterceptor is for catching save operations, and then saving a backup object to the database. It works if the object implements Auditable --if(args0 instanceof Auditable)--, and if this is not a "insert into" but a "update" operation --if(oldId!=null)--.


public class SaveDatabaseInterceptor implements MethodInterceptor &#123;
private SystemDAO systemDAO = null;
private static final Log log = LogFactory.getLog&#40;SaveDatabaseInterceptor.class&#41;;

public void setSystemDAO&#40;SystemDAO systemDao&#41; &#123;
this.systemDAO = systemDao;
&#125;

public Object invoke&#40;MethodInvocation invocation&#41; throws Throwable &#123;
log.debug&#40;"save advice will work&#58;" + invocation&#41;;
BasePersistentObject bpo=null;
Object&#91;&#93; args=invocation.getArguments&#40;&#41;;
Object args0=args&#91;0&#93;;
if&#40;args0 instanceof Auditable&#41; &#123;
bpo=&#40;BasePersistentObject&#41;args0;
Long oldId=bpo.getId&#40;&#41;;
if&#40;oldId!=null&#41;&#123;
BasePersistentObject backupBPO=bpo.getBackupObject&#40;&#41;;
backupBPO.setDatabaseStatus&#40;BasePersistentObject.R ECORD_UPDATED+"_"+bpo.getBaseRecord&#40;&#41;.getId&#40;&#41;+"_"+&#40;bpo.getDatabaseVersion&#40;&#41;+1&#41;&#41;;
systemDAO.saveBackupBPO&#40;backupBPO&#41;;
log.debug&#40;"save advice worked&#58;" + invocation&#41;;
&#125;
else &#123;
//new record
bpo.setBaseRecord&#40;bpo&#41;;
&#125;
&#125;
Object rval = invocation.proceed&#40;&#41;;
return rval;
&#125;

Here is the BAsePersistentObject from which all my persistent classes inherit from:


public abstract class BasePersistentObject implements Serializable &#123;
public static String RECORD_ACTIVE="ACTIVE";
public static String RECORD_UPDATED="UPDATED";
public static String RECORD_DELETED="DELETED";

protected Long id;
protected BasePersistentObject baseRecord;
protected int databaseVersion;
protected String databaseStatus=RECORD_ACTIVE;
protected User createdUser;
protected Timestamp createdTime;
protected User lastModifiedUser;
protected Timestamp lastModifiedTime;
protected Timestamp systemModifiedTime;
protected User ownerUser;

// getters, setters omitted

public boolean isActive&#40;&#41; &#123; return getDatabaseStatus&#40;&#41;.equals&#40;"ACTIVE"&#41;; &#125;

public boolean isDeleted&#40;&#41; &#123; return getDatabaseStatus&#40;&#41;.equals&#40;"DELETED"&#41;; &#125;

public BasePersistentObject getBackupObject&#40;&#41; &#123;
BasePersistentObject bpo=null;
try&#123;
bpo=&#40;BasePersistentObject&#41;BeanUtils.cloneBean&#40;this &#41;;
bpo.setId&#40;null&#41;;
&#125;catch&#40;Exception e&#41; &#123;
System.out.println&#40;e&#41;;
&#125;
return bpo;
&#125;


The interesting method here is getBAckupObject(), which returns a copy of this entity for auditing purposes. The classes to be audited will have to implement a marker interface called Auditable.
Remove advice will simply modify the database status as "DELETED".



public class RemoveDatabaseInterceptor implements MethodInterceptor &#123;
private static final Log log = LogFactory.getLog&#40;RemoveDatabaseInterceptor.class&#41; ;
private SystemDAO systemDAO = null;

public void setSystemDAO&#40;SystemDAO systemDao&#41; &#123;
this.systemDAO = systemDao;
&#125;

public Object invoke&#40;MethodInvocation invocation&#41; throws Throwable &#123;
log.debug&#40;"remove advice will work&#58;" + invocation&#41;;
Object&#91;&#93; args=invocation.getArguments&#40;&#41;;
Object args0=args&#91;0&#93;;
if&#40;args0 instanceof Auditable&#41; &#123;
BasePersistentObject bpo=&#40;BasePersistentObject&#41;args&#91;0&#93;;
if&#40;!bpo.getDatabaseStatus&#40;&#41;.equals&#40;BasePersistentO bject.RECORD_ACTIVE&#41;&#41;
throw new Exception&#40;"Status of the entity object was not ACTIVE"&#41;;
else &#123;

bpo.setDatabaseStatus&#40;BasePersistentObject.RECORD_ DELETED+"_"+bpo.getBaseRecord&#40;&#41;.getId&#40;&#41;&#41;;
log.debug&#40;"remove advice worked&#58;" + invocation&#41;;
&#125;
&#125;

Object rval = invocation.proceed&#40;&#41;;
return rval;

&#125;

SystemDAO, which handles application wide DAO, looks like:


public interface SystemDAO extends DAO &#123;
/**
* When an exception occurs, we want to record it in the database,
* with use of AOP&#40;ExceptionAdvice&#41;. Not to be used directly, the system calls when needed.
* @param ael the ApplicationErrorLog object to be logged in the database.
* @return the ApplicationErrorLog object that was logged in the database.
* @see ExceptionAdvice
*/
public ApplicationErrorLog saveApplicationErrorLog&#40;ApplicationErrorLog ael&#41;;

/**
* All database create or update operations can use this interface.
* @param bpo the BasePersistentObject to be created or updated in the database.
* Subject to the AOP SaveDatabaseInterceptor if implements Auditable.
* @return the BasePersistentObject that was created or updated in the database.
* @see SaveDatabaseInterceptor
* @see Auditable
*/
public BasePersistentObject saveBPO&#40;BasePersistentObject bpo&#41;;

/**
* Backup records&#40;auditing&#41; are saved through this interface.
* Created to let SaveDatabaseInterceptor bypass this database update.
* @param bpo the BasePersistentObject to be created in the database for auditing purposes.
* @return the BasePersistentObject that was created in the database for auditing purposes..
* @see SaveDatabaseInterceptor
*/
public BasePersistentObject saveBackupBPO&#40;BasePersistentObject bpo&#41;;

/**
* All database "delete" operations use this interface.
* @param bpo the BasePersistentObject to be deleted in the database.
* Subject to the AOP RemoveDatabaseInterceptor if implements Auditable.
* @see RemoveDatabaseInterceptor
* @see Auditable
*/
public void removeBPO&#40;BasePersistentObject bpo&#41;;

/**
* All database "select * from ATABLE where id=?" operations use this interface.
* @param theClass The class name of the entity to be retrieved.
* @param id The Long representation of the identifier field
* @return the BasePersistentObject that was retrieved from the database.
*/
public BasePersistentObject retrieveBPO&#40;Class theClass,Long id&#41;;

And the Hibernate implementation:


public class SystemDAOHibernate extends HibernateDaoSupport implements SystemDAO &#123;
private static final Log log = LogFactory.getLog&#40;SystemDAOHibernate.class&#41;;

public ApplicationErrorLog saveApplicationErrorLog&#40;ApplicationErrorLog ael&#41; &#123;
getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;ael&#41;;
return ael;
&#125;

public BasePersistentObject saveBPO&#40;BasePersistentObject bpo&#41; &#123;
getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;bpo&#41;;
return bpo;
&#125;

public BasePersistentObject saveBackupBPO&#40;BasePersistentObject bpo&#41; &#123;
getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;bpo&#41;;
return bpo;
&#125;

public void removeBPO&#40;BasePersistentObject bpo&#41; &#123;
getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;bpo&#41;;
&#125;

public BasePersistentObject retrieveBPO&#40;Class theClass,Long id&#41;&#123;
return &#40;BasePersistentObject&#41;getHibernateTemplate&#40;&#41;.get&#40;t heClass,id&#41;;
&#125;

The ExceptionAdvice mentioned above looks like (inserts a record into the APPLICATIONERRORLOG table):



public class ExceptionAdvice implements ThrowsAdvice&#123;
private SystemDAO systemDAO = null;
private UserDAO userDAO = null;
private static final Log log = LogFactory.getLog&#40;ExceptionAdvice.class&#41;;

public void setUserDAO&#40;UserDAO userDao&#41; &#123;
this.userDAO = userDao;
&#125;
public void setSystemDAO&#40;SystemDAO systemDao&#41; &#123;
this.systemDAO = systemDao;
&#125;

public void afterThrowing&#40;Method m,Object&#91;&#93; args,Object target,Exception ex&#41;&#123;
log.debug&#40;"Exception advice will work!&#58;"+ex&#41;;
ApplicationErrorLog ael=new ApplicationErrorLog&#40;&#41;;
Timestamp now = new Timestamp&#40;System.currentTimeMillis&#40;&#41;&#41;;
ael.setCreatedTime&#40;now&#41;;

ael.setCreatedUser&#40;userDAO.retrieveUserByUserName&#40;"admin"&#41;&#41;;
ael.setDescription&#40;"Method&#58;"+m+" Exception&#58;"+ex&#41;;
ael.setErrorNumber&#40;"111"&#41;; //TODO&#58; To be implemented by looking at the actual error...
systemDAO.saveApplicationErrorLog&#40;ael&#41;;
log.debug&#40;"Exception advice worked!&#58;"+ex&#41;;
&#125;

The database schema for an Auditable class looks like:


create table DEPARTMENT&#40;
ID NUMERIC NOT NULL primary key,
DATABASEVERSION NUMERIC NOT NULL,
BASERECORDID NUMERIC NOT NULL,
DATABASESTATUS VARCHAR&#40;15&#41; DEFAULT 'ACTIVE' NOT NULL,
CREATEDUSERID NUMERIC NOT NULL,
CREATEDTIME TIMESTAMP NOT NULL,
LASTMODIFIEDUSERID NUMERIC NOT NULL,
LASTMODIFIEDTIME TIMESTAMP NOT NULL,
SYSTEMMODIFIEDTIME TIMESTAMP NOT NULL,
OWNERUSERID NUMERIC NOT NULL,
NAME VARCHAR&#40;20&#41; NOT NULL,
DESCRIPTION VARCHAR&#40;100&#41; NOT NULL&#41;;

And finally the applicationcontext.xml (I know this has been long already :))


<beans>
<!-- BASICDATASOURCE -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- datasource properties omitted -->
</bean>

<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFact oryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Hibernate definitions omitted -->
</bean>

<!-- Transaction manager for a single Hibernate SessionFactory &#40;alternative to JTA&#41; -->
<bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransac tionManager">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>

<bean id="DAOTemplate" lazy-init="true"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames">
<list>
<value>saveDatabaseAdvisor</value>
<value>removeDatabaseAdvisor</value>
</list>
</property>
</bean>

<bean id="userDAO" parent="DAOTemplate" >
<property name="proxyInterfaces"><value>dao.UserDAO</value></property>
<property name="target">
<bean id="userDAOTarget" class="dao.hibernate.UserDAOHibernate">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</property>
</bean>

<bean id="systemDAO" parent="DAOTemplate" >
<property name="proxyInterfaces"><value>dao.SystemDAO</value></property>
<property name="target">
<bean id="systemDAOTarget" class="dao.hibernate.SystemDAOHibernate">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</property>
</bean>

<bean id="TransactionalFacadeTemplate" lazy-init="true"
class="org.springframework.transaction.interceptor.Transa ctionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="transactionAttributes">
<props>
<prop key="update_*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_SUPPORTS,readOnly</prop>
</props>
</property>
</bean>

<bean id="SecureFacadeTemplate" lazy-init="true" parent="TransactionalFacadeTemplate"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames">
<list>
<value>securityAdvisor</value>
</list>
</property>
</bean>

<bean id="userManagerFacade" parent="SecureFacadeTemplate" >
<property name="proxyInterfaces"><value>service.UserManagerFacade</value></property>
<property name="target">
<bean id="userManagerFacadeTarget" class="service.impl.UserManagerFacadeImpl">
<property name="userDAO"><ref local="userDAO"/></property>
<property name="systemDAO"><ref local="systemDAO"/></property>
</bean>
</property>
</bean>

<bean id="saveDatabaseInterceptor" class="util.SaveDatabaseInterceptor">
<property name="systemDAO"><ref local="systemDAO"/></property>
</bean>

<bean id="saveDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointc utAdvisor">
<property name="advice"><ref local="saveDatabaseInterceptor"/></property>
<property name="patterns">
<list>
<!--<value>.*save.*</value>-->
<value>.*save&#40;?!Backup&#41;\w+</value>
</list>
</property>
</bean>

<bean id="removeDatabaseInterceptor" class="util.RemoveDatabaseInterceptor" >
<property name="systemDAO"><ref local="systemDAO"/></property>
</bean>

<bean id="removeDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointc utAdvisor">
<property name="advice"><ref local="removeDatabaseInterceptor"/></property>
<property name="patterns">
<list>
<value>.*remove.*</value>
</list>
</property>
</bean>

<bean id="securityInterceptor" class="util.SecurityInterceptor" />

<bean id="securityAdvisor" class="org.springframework.aop.support.RegexpMethodPointc utAdvisor">
<property name="advice"><ref local="securityInterceptor"/></property>
<property name="patterns">
<list>
<value>.*retrieve.*</value>
<value>.*update_.*</value>
</list>
</property>
</bean>

<bean id="exceptionAdvice" class="util.ExceptionAdvice" >
<property name="userDAO"><ref local="userDAO"/></property>
<property name="systemDAO"><ref local="systemDAO"/></property>
</bean>

<bean id="exceptionAdvisor" class="org.springframework.aop.support.RegexpMethodPointc utAdvisor">
<property name="advice"><ref local="exceptionAdvice"/></property>
<property name="patterns">
<list>
<value>.*</value>
</list>
</property>
</bean>

<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.Defaul tAdvisorAutoProxyCreator">
<property name="advisorBeanNamePrefix">
<value>exceptionAdv</value>
</property>
<property name="usePrefix">
<value>true</value>
</property>
</bean>

</beans>

And (really finally) for the sake of completeness, here is the SecurityInterceptor:


public class SecurityInterceptor implements MethodInterceptor &#123;
private static final Log log = LogFactory.getLog&#40;SecurityInterceptor.class&#41;;

public Object invoke&#40;MethodInvocation invocation&#41; throws Throwable &#123;
// Apply crosscutting code
doSecurityCheck&#40;invocation&#41;;

// Call next interceptor
return invocation.proceed&#40;&#41;;
&#125;

protected void doSecurityCheck&#40;MethodInvocation invocation&#41; throws UnAuthorizedException &#123;
log.debug&#40;"security check performed&#58;"+invocation&#41;; //TODO&#58; implement security check...
&#125;


You can follow the progress of this at http://sourceforge.net/projects/openhelpdesk if you want to.

Regards,
Turgay Zengin