【问题标题】:Generic Search Criteria Type通用搜索条件类型
【发布时间】:2012-08-02 04:45:38
【问题描述】:

我使用表示层、服务层和数据访问层构建了我的应用程序。有多种服务和 API 通常需要从数据访问层检索一些数据。为了获得这样的数据,我经常需要将一些搜索条件从一层传递到另一层。例如,我可以传递 from_date、to_date、email_id、user_id 等属性。现在,我尝试传递一个对象,比如 SearchCriteria,而不是传递很多单独的参数。同样,不同的服务/api 需要不同的 SearchCriterias。

问题是,我如何使其通用化,以使其可供所有服务使用。一种方法是拥有一个 SearchCriteria 对象,该对象包含一个 SEARCH_KEY 到值的映射,其中 SEARCH_KEY 可以是一个枚举(具有 FROM_DATE、TO_DATE 等值)。根据 SEARCH_KEY,我可以检索密钥并使用它。但这仅在您的一层知道该映射期望的值的类型(整数,字符串)时才有效。一层耦合到另一层,我认为这不好。我经历了这个问题 - Map with multiple value types with advantages of generics 解决了类似的问题。这是一个好的设计吗?有没有更好的选择?

【问题讨论】:

    标签: java


    【解决方案1】:

    如果您正在使用 JPA 之类的东西,您可能需要查看 JPA Criteria API。这样你就可以编写一个基本的过滤器类:

    public abstract class AbstractFilter<T> {
        private EntityManager em;
        private Class<T> entityClass;
        private List<Predicate> predicates;
        private CriteriaBuilder cb;
    
        public AbstractFilter(EntityManager em, Class<T> clazz) {...}
    
        protected void add(Predicate predicate) {
            if (predicates == null) predicates = new ArrayList<>();
            if (predicate != null) predicates.add(predicate);
        }
    
        protected <X> void addEqual(Expression<X> expression, X value) {
            if (expression != null && value != null && cb != null)
                add(cb.equal(expression, value));
        }
    
        protected abstract void buildPredicates(Root<T> root);
    
        public List<T> getResultList() {
            this.cb = em.getCriteriaBuilder();
            CriteriaQuery<T> query = cb.createCriteria(entityClass);
            Root<T> root = query.from(entityClass);
            buildPredicates(root);
            return em.createQuery(query.select(root).where(predicates)).getResultList();
        }
    }
    
    public class UserFilter<User> {
        private String firstName, lastName;  // plus getter and setter
        private Gender gender;               // plus getter and setter
    
        public UserFilter(EntityManager em) { super(em, User.class); }
    
        protected void buildPredicates(Root<User> user) {
            addEqual(user.get(User_.firstName), getFirstName());
            addEqual(user.get(User_.lastName), getLastName());
            addEqual(user.get(User_.gender), getGender());
        }
    }
    
    ------
    
    UserFilter filter = new UserFilter(entityManager);
    filter.setFirstName("Joe");
    filter.setGender(Gender.Male);
    List<User> users = filter.getResultList();
    

    这只是过滤器类的一个非常基本的示例,您可以向过滤器添加越来越多的addXXX(Expression, ?) 方法以及排序和分页等功能。请注意,如果要将其与其他层分开,则应通过 DAO 设置 EntityManager,例如作为 getResultList(EntityManager) 方法中的参数。


    另一种解决方案可能是这样的示例对象:

    User example = new User();
    example.setFirstName("J*");
    example.setGender(Gender.Male);
    List<User> users = userDao.find(example);
    

    您的 DAO 实现本身在哪里进行过滤。

    【讨论】:

    • 您建议的第一个解决方案适用于数据访问层,但我认为将休眠层的东西暴露给服务层并不是一个好主意。对于第二个解决方案,它类似于我的建议。我正在尝试是否可以使其足够通用(如地图)。
    • @SwapnilS 关于示例对象(第二个解决方案)的想法是保持它的类型安全。将 interface Dao&lt;T&gt; { List&lt;T&gt; find(T example); } 视为通用 DAO 接口。
    【解决方案2】:

    我刚刚使用新的条件 API 在 Hibernate 5 上进行了通用搜索这个解决方案。 我得到了一个类似于 JavaIO 的 FileFilter 的解决方案。

    在 GenericDAO,我实现了一个通用搜索的方法:

    public List<T> search(SearchFilter searchFilter) {
        CriteriaBuilder criteriaBuilder = getSession().getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(getPersistentClass());
        Root<T> entityRoot = criteriaQuery.from(getPersistentClass());
        criteriaQuery.select(entityRoot);
        criteriaQuery.where(searchFilter.buildPredicate(criteriaBuilder, entityRoot));
        Query<T> query = getSession()
                .createQuery(criteriaQuery);
        return query.getResultList();
    }
    

    对于 SearchFilter 类(作为 GenericDAO 的嵌套类实现),代码非常简单:

    abstract class SearchFilter {
        abstract protected Predicate buildPredicate(CriteriaBuilder criteriaBuilder, Root<T> root);
    }
    

    现在的用法是这样的:

    public List<Product> findByCategory(ProductCategory productCategory) {
        return search(new SearchFilter() {
            @Override
            protected Predicate buildPredicate(CriteriaBuilder criteriaBuilder, Root<Product> root) {
                return criteriaBuilder.equal(root.get("productCategory"), productCategory);
            }
        });
    }
    

    此解决方案允许通过更简单的实现来减少特定 DAO 搜索的代码行数。

    【讨论】:

      【解决方案3】:

      你可以这样做

      写 Enum 来保存你的参数键

      enum ParameterKeys {
      
        SERIAL_NUMBER,
        SEARCH_KEY,
        FROM_DATE,
        ISVALID
      
      }
      

      编写一个保存参数值的类

      public class Parameter {
      
          private Object value;
          private Class<?> clazz;
      
          public Parameter(Object value) {
              setValue(value);
          }
      
          public void setValue(Object value) {
              this.value = value;
              this.clazz = value.getClass();
          }
      
          public Object getValue() {
              return clazz.cast(value);
          }
      
      }
      

      现在尝试制作参数 Map

       public class Main {
      
              public static void main(String[] args) {
                  Map<String,Parameter> _params = new HashMap<String, Parameter>();
                  _params.put(ParameterKeys.SERIAL_NUMBER, new Parameter(1));
                       _params.put(ParameterKeys.SEARCH_KEY, new Parameter("query"));
                  _params.put(ParameterKeys.FROM_DATE, new Parameter(new Date()));
                  _params.put(ParameterKeys.ISVALID, new Parameter(1));
              System.out.println(_params.get(ParameterKeys.SERIAL_NUMBER).getValue().getClass().getCanonicalName());
          System.out.println(_params.get(ParameterKeys.SEARCH_KEY).getValue().getClass().getCanonicalName());     System.out.println(_params.get(ParameterKeys.FROM_DATE).getValue().getClass().getCanonicalName());    System.out.println(_params.get(ParameterKeys.FROM_DATE).getValue().getClass().getCanonicalName());
      
              }
      
          }
      

      输出将是:

      java.lang.Integer
      java.lang.String
      java.util.Date
      java.lang.Boolean
      

      您可以将此地图传递到不同的关卡

      【讨论】:

      • 如何防止为某个键添加类型不正确的值。例如,_params.put(ParameterKeys.FROM_DATE, "someString") 在编译时不会失败,对吗?
      • 不,它不会在编译时失败。在运行时解决它对你来说足够了吗?
      • 这不会是一个问题,但我不会冒险在运行时失败。
      【解决方案4】:

      如果在运行时处理就足够了,你可以试试这个

      public enum ParameterKeys {
      
        SERIAL_NUMBER {
            public Class<?> getType() {
                return Integer.class;
            }
        },
        SEARCH_KEY{
            public Class<?> getType() {
                return String.class;
            }
        },
        FROM_DATE{
            public Class<?> getType() {
                return Date.class;
            }
        },
        ISVALID{
            public Class<?> getType() {
                return Boolean.class;
            }
        };
      
      public abstract Class<?> getType();
      

      }

      public class Parameter {
      
      private Object value;
      private Class<?> clazz;
      private ParameterKeys key;
      
      public Parameter(Object value,ParameterKeys key) throws Exception {
          this.key = key;
          setValue(value);
      }
      
      public void setValue(Object value) throws Exception{
          this.value = value;
          this.clazz = value.getClass();
          if(!clazz.getCanonicalName().equals(key.getType().getCanonicalName())) {
              throw new Exception("Type Mismatch between Parameter Key : " + key + " and Value : " + value);
          }
      }
      
      public Object getValue() {
          return clazz.cast(value);
      }
      

      }

      public class Main {
      
      public static void main(String[] args) {
          Map<ParameterKeys,Parameter> _params = new HashMap<ParameterKeys, Parameter>();
          try {
          _params.put(ParameterKeys.SERIAL_NUMBER, new Parameter(1,ParameterKeys.SERIAL_NUMBER));
          _params.put(ParameterKeys.SEARCH_KEY, new Parameter("query",ParameterKeys.SEARCH_KEY));
          _params.put(ParameterKeys.FROM_DATE, new Parameter(new Date(),ParameterKeys.FROM_DATE));
          _params.put(ParameterKeys.FROM_DATE, new Parameter(true,ParameterKeys.FROM_DATE));
          } catch(Exception e) {
              System.out.println(e.getMessage());
          }
      
      }
      

      }

      这将打印出以下异常

      参数键:FROM_DATE 和值:true 之间的类型不匹配

      【讨论】:

      • 正如我在上一条评论中提到的,确保编译时类型检查总是更好。
      猜你喜欢
      • 2015-07-31
      • 2011-10-11
      • 1970-01-01
      • 2017-02-28
      • 2011-06-28
      • 1970-01-01
      • 2023-03-31
      • 2012-01-11
      • 1970-01-01
      相关资源
      最近更新 更多