Custom auditing with spring and hibernate on specific field

There are multiple ways to audit table fields with hibernate envers @Audited annotation. If this is used at entity level it will store audit of all fields like below(Example 1)
@Audited
public class SampleClass {

    @Id
    private Long id;


    @Column(name = "field_name_1")
    private String fieldName1;

    .
    .
    .
}

Example 1

If @Audited is used at field level, it will store audit of that specific field. In below example(Example 2) it will store audit of only fieldName2 field.
public class SampleClass {

    @Id
    private Long id;

    @Column(name = "field_name_1")
    private String fieldName1;

    @Audited
    @Column(name = "field_name_2")
    private String fieldName2;

    .

} 
Example 2


But in above two example, it will store the audit of all(Example 1) or few fields(Example 2) on change of ANY field. in example 2 if fieldName1 is changed then also it will create row in audit table with recent value of fieldName2. 

Sometimes we may encounter use case where we want to audit only when specific field is changed, we can achieve this without performance hit in following way.

  • add transient field.
  • copy previous copy to another new object.
  • async save class to store previous copy.


Since our save method is async another thread will perform audit save operation.

public class SampleClass {

    @Id
    private Long id;

    @Column(name = "field_name_1")
    private String fieldName1;
    
    @Audited
    @Column(name = "field_name_2")
    private String fieldName2;

    @Transient
    private SampleClass prevSampleClass;

    @PostLoad
    private void storePrevCopy() {
        org.springframework.beans.BeanUtils.BeanUtils.copyProperties(this, new SampleClass(), "prevSampleClass");
    }


    @PostPersist
    @PostUpdate
    private void logAudit() {
        CustomAuditLogService service =
                ApplicationContextAwareConfig.getApplicationContext()
                  .getAutowireCapableBeanFactory().getBean(CustomAuditLogService.class);
        service.save(prevSampleClass, this);
    }

} 

@Service
public class CustomAuditLogService {

    @Autowired
    private SampleClassAuditRepository sampleClassAuditRepository;

    @Async
    public void save(SampleClass prevSample, SampleClass newSample) {
        List skipFields = new ArrayList<>();
        skipFields.addAll(Arrays.asList("prevSampleClass","a", "b"));
        boolean auditToBeSaved = false;

        if (null != prevSample) {
            List changedFields = getChangedFields(prevSample, newSample, SampleClass.class, skipFields);
            if (!CollectionUtils.isEmpty(changedFields)) {
                auditToBeSaved = true;
            }

        } else {
            auditToBeSaved = true;
        }

        if (auditToBeSaved) {
            SampleClassAudit sampleAudit = new SampleClassAudit();
            BeanUtils.copyProperties(newSample, sampleAudit);
            vntAuditRepository.save(sampleAudit);
        }
    }



      private List getChangedFields(T obj1, T obj2, Class clazz, List skipFields) {
        List fields = getAllFields(new ArrayList<>(), clazz);
        List changedFields = new ArrayList<>();

        for (Field field : fields) {
            try {
                if (!skipFields.contains(field.getName())) {
                    PropertyDescriptor propertyDescriptor = new PropertyDescriptor(field.getName(), clazz);
                    Object o1Value = propertyDescriptor.getReadMethod().invoke(obj1);
                    Object o2Value = propertyDescriptor.getReadMethod().invoke(obj2);

                    if (null != o1Value && null != o2Value && !o1Value.equals(o2Value)) {
                        changedFields.add(field.getName());
                    } else if ((null == o1Value && null != o2Value) || (null != o1Value && null == o2Value)) {
                        changedFields.add(field.getName());
                    }

                }

            } catch (Throwable t) {
                log.error("Field {} could not be compared", field.getName(), t);
            }

        }
        return changedFields;
    }
}


If you have doubts or need any clarification, please let me know in comments.

Comments

Popular posts from this blog

Drools spring hibernate load rules from database