2013年2月1日 星期五

Update Specific Columns Automatically by Hibernate Interceptor

如果我們想要在某些時機點固定更新某些欄位,但是又不想讓這種邏輯被寫死在DAO裡面,就可以利用Hibernate Interceptor這個功能完成這件事情。

舉例來說,如果我們想要記錄每筆資料是誰在什麼時間產生,又是誰在什麼時間做了修改,如果在每個DAO中去處理這件事情就需要花相當的力氣,而Hibernate Interceptor會是個不錯的選擇。

以下我們就實作這樣的例子,程式分為三大部分:
  1. 一組攔截generic property的Hibernate Interceptor API
  2. 記錄誰在什麼時間更新資料的API
  3. 設定與程式套用範例
Part I
EmbeddablePropertyInterceptor
package com.gss.gmo.cao.hibernate.interceptor;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;

import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import lombok.ToString;

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.springframework.util.ReflectionUtils;

/**
 * @author linus_chien
 *
 * @param <Embeddable>
 */
@EqualsAndHashCode(of = "embeddableClass", callSuper = false)
@ToString(of = "embeddableClass")
public abstract class EmbeddablePropertyInterceptor<Embeddable> extends EmptyInterceptor {

    private static final long serialVersionUID = 1L;

    private Class<Embeddable> embeddableClass;

    @SuppressWarnings("unchecked")
    public EmbeddablePropertyInterceptor() {
        java.lang.reflect.Type t = getClass().getGenericSuperclass();
        ParameterizedType pt = (ParameterizedType) t;
        embeddableClass = (Class<Embeddable>) pt.getActualTypeArguments()[0];
    }

    /**
     * @return new Embeddable instance.
     */
    @SneakyThrows
    private Embeddable newInstance() {
        return embeddableClass.newInstance();
    }

    @SuppressWarnings("unchecked")
    private Embeddable findEmbeddableProperty(Object entity, Object[] state, String[] propertyNames) {
        for (int i = 0; i < propertyNames.length; i++) {
            String propertyName = propertyNames[i];
            Field auditInfoField = ReflectionUtils.findField(entity.getClass(), propertyName, embeddableClass);
            if (auditInfoField != null) {
                if (state[i] == null) {
                    state[i] = newInstance();
                }
                return (Embeddable) state[i];
            }
        }
        return null;
    }

    @Override
    public final boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        Embeddable embeddable = findEmbeddableProperty(entity, state, propertyNames);
        if (embeddable != null) {
            takeCreateEvent(embeddable);
            return true;
        } else {
            return false;
        }
    }

    protected abstract void takeCreateEvent(Embeddable embeddable);

    @Override
    public final boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        Embeddable embeddable = findEmbeddableProperty(entity, currentState, propertyNames);
        if (embeddable != null) {
            takeModifiedEvent(embeddable);
            return true;
        } else {
            return false;
        }
    }

    protected abstract void takeModifiedEvent(Embeddable embeddable);

}
EmbeddablePropertyInterceptorChainProxy
package com.gss.gmo.cao.hibernate.interceptor;

import java.io.Serializable;
import java.util.LinkedHashSet;
import java.util.Set;

import lombok.extern.apachecommons.CommonsLog;

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;

@CommonsLog
public class EmbeddablePropertyInterceptorChainProxy extends EmptyInterceptor {

    private static final long serialVersionUID = 5566719359520070978L;

    private Set<EmbeddablePropertyInterceptor<?>> interceptorChain = new LinkedHashSet<EmbeddablePropertyInterceptor<?>>();

    public void setInterceptorChain(Set<EmbeddablePropertyInterceptor<?>> interceptorChain) {
        this.interceptorChain.addAll(interceptorChain);
    }

    public void addInterceptorChain(EmbeddablePropertyInterceptor<?> interceptor) {
        interceptorChain.add(interceptor);
    }

    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        boolean isStateChanged = false;
        for (EmbeddablePropertyInterceptor<?> interceptor : interceptorChain) {
            log.debug("invoke onFlushDirty of " + interceptor.toString());

            if (interceptor.onFlushDirty(entity, id, currentState, previousState, propertyNames, types)) {
                isStateChanged = true;
            }
        }
        return isStateChanged;
    }

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        boolean isStateChanged = false;
        for (EmbeddablePropertyInterceptor<?> interceptor : interceptorChain) {
            log.debug("invoke onSave of " + interceptor.toString());

            if (interceptor.onSave(entity, id, state, propertyNames, types)) {
                isStateChanged = true;
            }
        }
        return isStateChanged;
    }

}
小結:
在EmbeddablePropertyInterceptor裡面我們留了兩個abstract method,讓developer各自實作。
EmbeddablePropertyInterceptorChainProxy是為了可以攔截不同的property,但限制是type不能重複。

Part II
AuditInterceptor
package com.gss.gmo.cao.hibernate.audit;

import java.util.Date;

import org.springframework.beans.factory.annotation.Required;

import com.gss.gmo.cao.hibernate.interceptor.EmbeddablePropertyInterceptor;

/**
 * Auto-set values of createUserId, createDate, lastModifiedUserId and
 * lastModifiedDate.
 *
 * @author linus_chien
 *
 */
public class AuditInterceptor extends EmbeddablePropertyInterceptor<AuditInfo> {

    private static final long serialVersionUID = 1L;

    /**
     * Provide audit user id.
     */
    private AuditUserIdAware auditUserIdAware;

    @Required
    public void setAuditUserIdAware(AuditUserIdAware auditUserIdAware) {
        this.auditUserIdAware = auditUserIdAware;
    }

    @Override
    protected void takeCreateEvent(AuditInfo auditInfo) {
        auditInfo.setCreateUserId(auditUserIdAware.getAuditUserId());
        auditInfo.setCreateUserName(auditUserIdAware.getAuditUserName());
        auditInfo.setCreateUserEnglishName(auditUserIdAware.getAuditUserEnglishName());
        auditInfo.setCreateDate(new Date());
    }

    @Override
    protected void takeModifiedEvent(AuditInfo auditInfo) {
        auditInfo.setLastModifiedUserId(auditUserIdAware.getAuditUserId());
        auditInfo.setLastModifiedUserName(auditUserIdAware.getAuditUserName());
        auditInfo.setLastModifiedUserEnglishName(auditUserIdAware.getAuditUserEnglishName());
        auditInfo.setLastModifiedDate(new Date());
    }

}
AuditUserIdAware
package com.gss.gmo.cao.hibernate.audit;

import java.io.Serializable;

/**
 * @author linus_chien
 *
 */
public interface AuditUserIdAware extends Serializable {

    /**
     * @return user id for auditing.
     */
    String getAuditUserId();

    /**
     * @return user name for auditing.
     */
    String getAuditUserName();

    /**
     * @return user English name for auditing.
     */
    String getAuditUserEnglishName();

}
AuditInfo
package com.gss.gmo.cao.hibernate.audit;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Embeddable;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@ToString
@Embeddable
public class AuditInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * column length.
     */
    private static final int LENGTH = 50;

    /**
     * create user id.
     */
    @Column(name = "CREATE_USER_ID", length = LENGTH)
    private String createUserId;

    /**
     * create user name.
     */
    @Column(name = "CREATE_USER_NAME", length = LENGTH)
    private String createUserName;

    /**
     * create user English name.
     */
    @Column(name = "CREATE_USER_ENGLISH_NAME", length = LENGTH)
    private String createUserEnglishName;

    /**
     * create date.
     */
    @Column(name = "CREATE_DATE")
    private Date createDate;

    /**
     * last modified user id.
     */
    @Column(name = "LAST_MODIFIED_USER_ID", length = LENGTH)
    private String lastModifiedUserId;

    /**
     * last modified user name.
     */
    @Column(name = "LAST_MODIFIED_USER_NAME", length = LENGTH)
    private String lastModifiedUserName;

    /**
     * last modified English name.
     */
    @Column(name = "LAST_MODIFIED_USER_ENGLISH_NAME", length = LENGTH)
    private String lastModifiedUserEnglishName;

    /**
     * last modified date.
     */
    @Column(name = "LAST_MODIFIED_DATE")
    private Date lastModifiedDate;

}
小結:
讓Hibernate Interceptor幫我們攔截AuditInfo;AuditUserIdAware是為了loose coupling,不讓取得使用者資訊的程式碼出現在AuditInterceptor裡面。

Part III
Person
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;

import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Type;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
@Filter(name = "personFilter", condition = "name like :name and address is null")
public class Person extends BaseDto {

    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String address;

    @Embedded
    private AuditInfo auditInfo;

    @Type(type = "listType")
    private List<String> emails;

}
LoginUserIdProvider
package com.gss.gmo.cao.hibernate.impl;

import com.gss.gmo.cao.hibernate.audit.AuditUserIdAware;

public class LoginUserIdProvider implements AuditUserIdAware {

    private static final long serialVersionUID = -5133132384619280103L;

    @Override
    public String getAuditUserId() {
        return "linus_chien";
    }

    @Override
    public String getAuditUserName() {
        return "Cheng-Ming Chien";
    }

    @Override
    public String getAuditUserEnglishName() {
        return "Linus";
    }

}
SpringConfig
package com.gss.gmo.cao.hibernate.impl;

import org.hibernate.Interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.gss.gmo.cao.hibernate.audit.AuditInterceptor;
import com.gss.gmo.cao.hibernate.interceptor.EmbeddablePropertyInterceptorChainProxy;

@Configuration
public class SpringConfig {

    @Bean
    public Interceptor embeddablePropertyInterceptorChainProxy() {
        EmbeddablePropertyInterceptorChainProxy interceptor = new EmbeddablePropertyInterceptorChainProxy();
        AuditInterceptor auditInterceptor = new AuditInterceptor();
        auditInterceptor.setAuditUserIdAware(new LoginUserIdProvider());
        interceptor.addInterceptorChain(auditInterceptor);
        return interceptor;
    }

}
Spring XML
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource">
        <ref bean="dataSource" />
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
            <prop key="hibernate.hbm2ddl.auto">create</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.format_sql">true</prop>
        </props>
    </property>
    <property name="packagesToScan" value="com.gss.gmo.cao.hibernate.impl" />
    <property name="annotatedPackages" value="com.gss.gmo.cao.hibernate.impl" />
    <property name="entityInterceptor" ref="embeddablePropertyInterceptorChainProxy" />
</bean>
小結:
LoginUserIdProvider裡面可以和Spring Security整合,取得目前登入的使用者資訊。