2013年1月24日 星期四

Enable Hibernate Filter Dynamically

Hibernate Filter功能的主要目的是預先設定好filter definition和filter condition,然後在操作Hibernate session時決定要不要enable filter並提供要傳入condition的變數,將一些公用的condition抽離到filter中,Hibernate會在符合filter設定的任何查詢上都補上定義好的condition。

但這樣還不夠好,原因是這樣的使用方式會讓DAO裡面充滿enable filter的程式碼,developer必須了解這些filter的規範才能正確使用,如果我們會依照使用者的身份決定condition的值,那麼與security有關的API就會汙染DAO,為此,我們利用Spring AOP的功能來完成enable filter dynamically的機制,讓interceptor來決定目前被攔截的DAO是否需要enable filter並傳入condition變數。

以下的程式碼分為兩大部分,interceptor and test case。

Interceptor:
EnableFilterInterceptor
package com.gss.gmo.cao.hibernate.filter;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;

import lombok.SneakyThrows;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.ReflectionUtils;

import com.gss.gmo.cao.hibernate.HibernateGenericDao;
import com.gss.gmo.cao.hibernate.HibernateGenericPagingDao;

/**
 * @author linus_chien
 *
 */
public class EnableFilterInterceptor implements MethodInterceptor {

    /**
     * Provide filter name and parameters to enable.
     */
    private EnableFilterInfoAware enableFilterInfoAware;

    /**
     * @param enableFilterInfoAware
     */
    @Required
    public void setEnableFilterInfoAware(EnableFilterInfoAware enableFilterInfoAware) {
        this.enableFilterInfoAware = enableFilterInfoAware;
    }

    @Override
    @SneakyThrows
    public Object invoke(MethodInvocation invocation) {
        Object target = invocation.getThis();
        if (target instanceof HibernateGenericDao || target instanceof HibernateGenericPagingDao) {
            SessionFactory sessionFactory = getSessionFactory(target);
            Session session = sessionFactory.getCurrentSession();
            @SuppressWarnings("unchecked")
            Set<String> filterNames = sessionFactory.getDefinedFilterNames();
            Map<String, Map<String, Object>> filterInfo = enableFilterInfoAware.getEnableFilterInfo();
            for (String filterName : filterNames) {
                if (filterInfo.containsKey(filterName)) {
                    Filter filter = session.getEnabledFilter(filterName);
                    if (filter == null) {
                        filter = session.enableFilter(filterName);
                        Map<String, Object> parameters = filterInfo.get(filterName);
                        for (String parameterName : parameters.keySet()) {
                            Object parameterValue = parameters.get(parameterName);
                            filter.setParameter(parameterName, parameterValue);
                        }
                        filter.validate();
                    }
                }
            }
        }

        Object result = invocation.proceed();
        return result;
    }

    /**
     * @param target
     * @return SessionFactory
     */
    private SessionFactory getSessionFactory(Object target) {
        Field sessionFactoryField = ReflectionUtils.findField(target.getClass(), "sessionFactory", SessionFactory.class);
        ReflectionUtils.makeAccessible(sessionFactoryField);
        return (SessionFactory) ReflectionUtils.getField(sessionFactoryField, target);
    }

}
EnableFilterInfoAware
package com.gss.gmo.cao.hibernate.filter;

import java.util.Map;

/**
 * @author linus_chien
 *
 */
public interface EnableFilterInfoAware {

    /**
     * Key: filter name, Value: parameters (Key: parameter name, Value: value).
     *
     * @return
     */
    Map<String, Map<String, Object>> getEnableFilterInfo();

}
EnableFilterInfoAware的用意事實上就是Strategy Pattern的一種應用,再一次將怎麼enable filter這件事交給strategy object負責。

Test Case包含四小部分:
  1. Filter Definition:package-info.java
  2. Filter:Person
  3. Implement Strategy:EnableFilterInfoProvider
  4. Setup AOP:Spring XML
package-info.java
/**
 * @author linus_chien
 *
 */
@FilterDef(name = "personFilter", parameters = { @ParamDef(name = "name", type = "string"), @ParamDef(name = "address", type = "string") })
@TypeDef(name = "listType", typeClass = ListJsonType.class)
package com.gss.gmo.cao.hibernate.impl;

import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
import org.hibernate.annotations.TypeDef;
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;

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

import java.util.HashMap;
import java.util.Map;

import com.gss.gmo.cao.hibernate.filter.EnableFilterInfoAware;

public class EnableFilterInfoProvider implements EnableFilterInfoAware {

    @Override
    public Map<String, Map<String, Object>> getEnableFilterInfo() {
        Map<String, Map<String, Object>> enableFilterInfo = new HashMap<String, Map<String, Object>>();
        Map<String, Object> parameter = new HashMap<String, Object>();
        parameter.put("name", "%Linus%");
        parameter.put("address", "abc");
        enableFilterInfo.put("personFilter", parameter);
        return enableFilterInfo;
    }

}
Spring XML
<aop:config>
    <aop:pointcut id="daoPointcut" expression="execution(* com.gss.gmo.cao.hibernate.*Dao.*(..))" />
    <aop:advisor advice-ref="daoAdvice" pointcut-ref="daoPointcut" />
</aop:config>

<bean id="daoAdvice" class="com.gss.gmo.cao.hibernate.filter.EnableFilterInterceptor">
    <property name="enableFilterInfoAware">
        <bean class="com.gss.gmo.cao.hibernate.impl.EnableFilterInfoProvider" />
    </property>
</bean>
這樣只要是被攔截到的DAO就會enable filter,然後只要查詢Person就會補上額外的condition,而相同的filter definition套在每個entity都能有不同的condition。想像一下,如果今天要做到個人只能看個人的資料,而主管可以看到部門內所有人的資料,利用這個方式就不需要在DAO裡面implement這樣的邏輯,而是交給Hibernate Filter和Strategy處理了。

最後我們看一下Hibernate實際下的SQL statement:
Show SQL
Hibernate:
    select
        count(*) as y0_
    from
        Person this_
    where
        this_.name like ? 
        and this_.address is null
        and (
            this_.name=?
        )