Monday, July 29, 2013

Nested Hibernate Filters

Nested Hibernate Filters

Hibernate added a feature that is not part of the JPA, and this is filters.
When you want to load information from the database but want to filter the data, you can use HQL, @Where or filters.
HQL is good if your filter is limited to the query that you need. If you always want to filter the class you can add to the class the @Where. In the cases that you sometimes want to filter and sometimes not, or you need to parameterize the filtering then you use the @Filters.
It feels like all the features of filtering were an afterthought by hibernate. The reason is that hibernate does not use the field names for the filtering or @Where but uses the database field name. The reason they do this, is that it is easy to add to the sql statement the where statement, and they do not have to parse the class objects.
Another oversight on the filter issue is nested filters. Hibernate does not care how many times you enable the filter, one disable will disable it in any case. Also if you change the parameter of a filter that is already open hibernate has no problem and will update the fiter.

Take the following case:

Enable filter, date: 1/1/2000
Load data
Enable filter, date: 1/1/1999
Load data
                Disable filter
Load data
Disable filter

In this scenario we have two problems. Once the disable filter is called there is no open filter so the third load data will run with no filter at all. Even if the filter was not closed we would still have the problem that the date of the filter is incorrect, since it is set for 1999 and not 2000.
To solve this issue we need to implement a stack of filters per filter name and store them on the local thread (so that our genericdao is thread safe).

FilterInfo Class

The first class will hold the parameters of the filter and what is the nested count (how many times has the enable been called for the same filter with the same parameters):
public class FilterInfo {
       private int nestedCount;
       private Map<String, Object> parameters = new HashMap<String, Object>();
      
             
       public FilterInfo(Map<String, Object> parameters) {
              super();
              this.parameters = parameters;
       }

      
       public boolean areParametersTheSame(Map<String, Object> currentParameters) {
              if (parameters.size() > 0) {
                     // check old parameters, see if changed
                     for (String paramName : parameters.keySet()) {
                           for (String currentparamName : currentParameters.keySet()) {
                                  if (paramName.equals(currentparamName)) {
                                         Object newValue = parameters.get(paramName);
                                         Object oldValue = currentParameters.get(paramName);
                                         if (!newValue.equals(oldValue)) {
                                                return false;
                                         }
                                  }
                           }
                     }
              }
              return true;
       }

       public int decNestedCount() {
              nestedCount--;
              return nestedCount;
       }

       public int incNestedCount() {
              nestedCount++;
              return nestedCount;
       }

       public Map<String, Object> getParameters() {
              return parameters;
       }

       public int getNestedCount() {
              return nestedCount;
       }

}

FilterStack Class

This class will store a stack of the parameters per filter so that we can re enable the filter with the correct parameters once a nested filter is disabled.
public class FilterStack {
       private String filterName;
       private Stack<FilterInfo> filterStack = new Stack<FilterInfo>();

       public FilterStack(String filterName) {
              this.filterName = filterName;
       }

       public FilterInfo setParameters(Map<String, Object> parameters) {
              boolean areParametersTheSame = false;
              FilterInfo filterInfo = null;
              if (filterStack.size() > 0) {
                     filterInfo = filterStack.peek();
                     areParametersTheSame = filterInfo.areParametersTheSame(parameters);
              }
              if (!areParametersTheSame) {
                     // add a new stack
                     filterInfo = new FilterInfo(parameters);
                     filterStack.push(filterInfo);
              }
              filterInfo.incNestedCount();
              return filterInfo;
       }

       public FilterInfo disableFilter() {
              if (filterStack.size()==0) {
                     throw new InfraException("you are closing a filter that is not open: " + filterName);
              }
              FilterInfo peek = filterStack.peek();
              if (peek != null) {
                     int decNestedCount = peek.decNestedCount();
                     if (decNestedCount <= 0) {
                           filterStack.pop();
                           return filterStack.size()>0 ?  filterStack.peek() : null;
                     }
              }
              return peek;
       }

}

NestedFilterInfo Class

The last class will be stored on the local thread, and will hold all the information for all the open filters.
public class NestedFilterInfo {
       private Map<String, FilterStack> filtersByName = new HashMap<String, FilterStack>();

       public FilterInfo enableFilter(String filterName, Map<String, Object> parameters) {
              // get current filter information
              FilterStack filterStack = filtersByName.get(filterName);
              if (filterStack == null) {
                     filterStack = new FilterStack(filterName);
                     filtersByName.put(filterName, filterStack);
              }

              // check if filter parameters have changed.
              return filterStack.setParameters(parameters);
       }

       public FilterInfo disableFilter(String filterName) {
              FilterStack filterStack = filtersByName.get(filterName);
              return filterStack.disableFilter();
       }
      
       public Set<String> getFilterNames() {
              return filtersByName.keySet();
       }

       public void clear() {
              filtersByName.clear();           
       }
}


GenericDao class

In the generic dao class that actually opens and closes the filter will use these classes to manage all open filters. The function to enable a filter will call:
public void enableFilter(String filterName, Map<String, Object> mapParameters) {
NestedFilterInfo filterStack = getFilterStack();
       FilterInfo enableFilter = filterStack.enableFilter(filterName, mapParameters);
       sessionEnableFilter(filterName, enableFilter.getParameters());
}

private void sessionEnableFilter(String filterName, Map<String, Object> mapParameters) {
       Filter filter = session().enableFilter(filterName);
       Object value;
       if (mapParameters != null) {
              for (Map.Entry<String, Object> entryParameters : mapParameters.entrySet()) {
                     value = entryParameters.getValue();
                     filter.setParameter(entryParameters.getKey(), value);
              }
       }
}

The enabling filter is the easy part. For every enable filter if no parameters have changed we will increment the counter on the FilterInfo class. If the parameters have changed we will create a new FIlterInfo and add it to the stack in the FilterStack.
The magic is done in the disable filter:
public void disableFilter(String filterName) {
       NestedFilterInfo filterStack = getFilterStack();
       FilterInfo filterInfo = filterStack.disableFilter(filterName);
       if (filterInfo == null || filterInfo.getNestedCount() <= 0) {
              session().disableFilter(filterName);
       } else {
              sessionEnableFilter(filterName, filterInfo.getParameters());
       }
}

Here we call the disable and get a FilterInfo class. If the nested count is down to zero then we have finished the stack and called the disable the same amount as the enable so we will close the filter on the session. If not then either the stack count went down by one, or we got the previous filter that was open. In either case we update the session filter with the parameters, and this way we have reverted the filter to the previous one.
This solution support both nested filters with the same parameters (only after the last disable with the filter really be closed) and nested filters with different parameters (with the new parameters the session will be updated, and on the disable filter the previous parameters will be returned to the session).








No comments:

Post a Comment