论坛首页 Java企业应用论坛

一个关于Hibernate的优化实例:从HQL到QBC,从QBC到QBE,再到“增强的”QBE

浏览 43219 次
该帖已经被评为良好帖
作者 正文
   发表时间:2008-08-28  
先解释一下标题的含义:为了实现一个组合条件查询,先是使用HQL书写,然后改用Query by Criteria方式,再尝试Query by Example,最后自己实现了一个增强的Example类来解决问题。

关于此问题的起源请阅读我以前的一个帖子:http://www.iteye.com/post/523791。在该帖子中已经实现了从HQL到QBC的转变,在这里就不再重复了。

在上一个帖子中没有模型类Product及Category的代码,为了方便讨论补充如下:

public class Category {
  private Long id;
  private String name; //类别名称

  //Other code omitted
}

public class Product {
  private Long id;
  private String name;       //商品名称
  private Category category; //商品类别
  private Date expDate;      //有效期
  private Float price;       //单价

  //Other code omitted
}


从前一个帖子中可以看到,使用QBC后代码有所减少,但还是得把构造查询条件的代码写死,这非常不爽。重读了《Java Persistence with Hibernate》一书,发觉QBE是个好东东,于是尝试用改造代码如下:

	public List<Product> getProducts(Product product) {
		final Example exampleProduct =
		  Example.create(product).
		    enableLike(MatchMode.ANYWHERE).
                    excludeZeroes();

		return (List<Product>) getHibernateTemplate().execute(
		  new HibernateCallback() {
		    public Object doInHibernate(Session session) throws HibernateException {
		    	Criteria crit =
		    	  session.createCriteria(Product.class).
		    	  add(exampleProduct);
		    	return crit.list();
		    }
		  }
		);
	}


代码非常简洁啊!我只要new一个Product实例,然后把要查询的条件值赋值到相应到属性上,如果某项条件未指定则相应的属性保留为默认的空值,将该实例传递给上面的getProducts方法,就能得到需要的结果了。超爽!

但是我却没办法把这段代码用在产品中,这是因为QBE有着严重的局限性:
1.不能查询指定在关联对象的属性上的条件。比如我想仅列出商品类别名称包括xyz的商品,代码如下:

Category category = new Category();
category.setName("xyz");
Product product = new Product();
product.setCategory(category);

List<Product> products = getProducts(product);


运行这段代码会列出所有的商品。

2.除了字符串条件可以调用enableLike()方法改用模糊查询外,其它数据类型的条件都只能等值比较。比如我无法查询所有有效的商品(有效期≥当前日期)。

难道就没有办法了吗?经过一番搜索,终于在Hibernate的官网论坛上找到一篇文章:http://forum.hibernate.org/viewtopic.php?t=942872。在该文章中,Dencel写了一个AssociationExample,经过大家的完善,终于解决了查询指定在关联对象的属性上的条件的问题。其主要的奥妙在于:

//Hibernate的原版Example
//如果属性类型是关联的实体,则忽略
	private boolean isPropertyIncluded(Object value, String name, Type type) {
		return !excludedProperties.contains(name) &&
			!type.isAssociationType() &&
			selector.include(value, name, type);
	}

//改版的AssociationExample
  private boolean includeAssociations = true;

  public boolean isIncludeAssociations()
  {
    return includeAssociations;
  }

  public void setIncludeAssociations(boolean includeAssociations)
  {
    this.includeAssociations = includeAssociations;
  }

//如果属性类型是关联的实体,且该关联是一对一或多对一,且includeAssociations为true,则包括该属性
  private boolean isPropertyIncluded(Object value, String name, Type type) {
    return
      !excludedProperties.contains(name) &&
      selector.include(value, name, type) &&
      (!type.isAssociationType() ||
        (type.isAssociationType() &&
          includeAssociations &&
          !type.isCollectionType()));
  }


解决了前面提到的第一个问题,第二个问题又怎么办呢?我想到一个办法:如果某个条件要使用其它的比较方式(比如大于等于),提供一个方法让用户为该属性指定比较方法,对于其它属性仍采用缺省的查询/比较方法:

//Hibernate原版的Example
  protected void appendPropertyCondition(
    String propertyName,
    Object propertyValue,
    Criteria criteria,
    CriteriaQuery cq,
    StringBuffer buf)
  throws HibernateException {
    Criterion crit;
    if ( propertyValue!=null ) {
//当属性值不为空时,如果是字符串且指定为模糊查询,则使用模糊查询,否则使用等值比较
      boolean isString = propertyValue instanceof String;
      SimpleExpression se = ( isLikeEnabled && isString ) ?
        Restrictions.like(propertyName, propertyValue) :
        Restrictions.eq(propertyName, propertyValue);
      crit = ( isIgnoreCaseEnabled && isString ) ?
        se.ignoreCase() : se;
    }
    else {
      crit = Restrictions.isNull(propertyName);
    }
    String critCondition = crit.toSqlString(criteria, cq);
    if ( buf.length()>1 && critCondition.trim().length()>0 ) buf.append(" and ");
    buf.append(critCondition);
  }


//增强后的EnhancedExample
  private static final RestrictionHolder holder = new DefaultRestrictionHolder();

  /**
   * Restriction strategy definitions
   */
  public static enum RestrictionStrategy {eq, ne, gt, lt, ge, le}

  /**
   * Restriction strategy holder for the query criteria
   */
  public static interface RestrictionHolder {
  	/**
  	 * Set a restriction strategy for a POJO's property
  	 */
    public RestrictionHolder set(String propertyName, RestrictionStrategy strategy);

    /**
     * Get the restriction strategy of the property
     */
    public RestrictionStrategy get(String propertyName);
  }

  static final class DefaultRestrictionHolder implements RestrictionHolder {
		private Map<String, RestrictionStrategy> strategies = new HashMap<String, RestrictionStrategy>();

    public RestrictionHolder set(String propertyName, RestrictionStrategy strategy) {
      strategies.put(propertyName, strategy);
      return this;
    }

    public RestrictionStrategy get(String propertyName) {
      return strategies.get(propertyName);
    }
  }

  /**
   * Get the restriction strategy holder
   */
  public RestrictionHolder getRestrictionHolder() {
    return holder;
  }

  protected void appendPropertyCondition(
    String propertyName,
    Object propertyValue,
    Criteria criteria,
    CriteriaQuery cq,
    StringBuffer buf)
  throws HibernateException {
    Criterion crit;
    if ( propertyValue!=null ) {
//当属性值不为空时,如果为该属性指定了比较条件,则使用指定的比较条件
      RestrictionStrategy strategy = holder.get(propertyName);
      if ( strategy != null ) {
  	switch(strategy) {
    	//case eq: crit = Restrictions.eq(propertyName, propertyValue);
    	case ne: crit = Restrictions.ne(propertyName, propertyValue); break;
    	case gt: crit = Restrictions.gt(propertyName, propertyValue); break;
    	case lt: crit = Restrictions.lt(propertyName, propertyValue); break;
    	case ge: crit = Restrictions.ge(propertyName, propertyValue); break;
    	case le: crit = Restrictions.le(propertyName, propertyValue); break;
    	default: crit = Restrictions.eq(propertyName, propertyValue);
        };
      }
      else {
//否则使用默认的比较条件:如果是字符串且指定为模糊查询,则使用模糊查询,否则使用等值比较
	boolean isString = propertyValue instanceof String;
	SimpleExpression se = ( isLikeEnabled && isString ) ?
	  Restrictions.like(propertyName, propertyValue) :
	  Restrictions.eq(propertyName, propertyValue);
	crit = ( isIgnoreCaseEnabled && isString ) ?
	  se.ignoreCase() : se;
      }
    }
    else {
      crit = Restrictions.isNull(propertyName);
    }
    String critCondition = crit.toSqlString(criteria, cq);
    if ( buf.length()>1 && critCondition.trim().length()>0 ) buf.append(" and ");
    buf.append(critCondition);
  }


于是前面getProducts方法只需要简单修改一下:

	public List<Product> getProducts(Product product) {
		//改用EnhancedExample来允许关联对象的条件查询
		final EnhancedExample exampleProduct =
		  EnhancedExample.create(product).
		    enableLike(MatchMode.ANYWHERE).
                    excludeZeroes();
		  //指定expDate属性使用大于等于比较方法
		exampleProduct.getRestrictionHolder().
		  set("expDate", EnhancedExample.RestrictionStrategy.ge);

		return (List<Product>) getHibernateTemplate().execute(
		  new HibernateCallback() {
		    public Object doInHibernate(Session session) throws HibernateException {
		    	Criteria crit =
		    	  session.createCriteria(Product.class).
		    	  add(exampleProduct);
		    	return crit.list();
		    }
		  }
		);
	}


经过以上改进,QBE的实用性大大提高,能够真正解决较大多数的组合查询问题。

已知的问题:以上“增强的”QBE还无法解决范围查询(比如价格在0到1000之间),这是因为一个属性只能携带一个值(你不可能指定两个值给Product.price属性)。这种情况下需要修改getProducts方法,增加参数把价格范围传递进来,再以QBC方式把相应的条件加到crit变量上。范例代码就不再给出了。

完整的EnhancedExample源码请见附件。
   发表时间:2008-08-28  
1.4不支持enum
0 请登录后投票
   发表时间:2008-08-28  
ray_linn 写道
浪费这么多行代码,做的都是无用功。

不要打击别人的激情,lz也只是给出了一个他认为最佳的方案,提供出来给大家讨论。实际上这个问题大家都遇到,每回给不同的查询单写一个方法,很费劲。这种通用查询还是很有价值的,如果能实现,能省掉很多的工作量。
0 请登录后投票
   发表时间:2008-08-28  
pn2008 写道
1.4不支持enum

不错,这些代码用到了枚举和泛型,需要JDK 5以上版本才能运行。如果你有兴趣,改成在JDK 1.4上运行也不会有什么困难吧?
0 请登录后投票
   发表时间:2008-08-28  
ray_linn 写道
浪费这么多行代码,做的都是无用功。

能否请你说明一下为何你认为这么做是浪费,是做无用功?对于文中提到的问题,你会怎么解决呢?
0 请登录后投票
   发表时间:2008-08-28  
pheh 写道
ray_linn 写道
浪费这么多行代码,做的都是无用功。

不要打击别人的激情,lz也只是给出了一个他认为最佳的方案,提供出来给大家讨论。实际上这个问题大家都遇到,每回给不同的查询单写一个方法,很费劲。这种通用查询还是很有价值的,如果能实现,能省掉很多的工作量。

虽然有点受打击,还是可以承受
不能说这是最佳方案,因为它不能解决所有的问题。但我认为它确实比Hibernate原版的Example进步了一些,所以提供出来给大家讨论,希望能够听到各位有建设性的建议,让我能改进它。如果各位有跟我的想法截然不同,但确实能解决我的问题的方案,那也很好啊
0 请登录后投票
   发表时间:2008-08-28  
这帖子挺不错的还投新手帖,对javaeye的大牛真是越来越不明白了
0 请登录后投票
   发表时间:2008-08-28  
这个不错,投个良好贴:D
ibatis可以很好的解决这类问题。
1 请登录后投票
   发表时间:2008-08-29  
cats_tiger 写道
这个不错,投个良好贴:D
ibatis可以很好的解决这类问题。

不懂ibatis,能否请你大致地描述一下解决办法或思路?
1 请登录后投票
   发表时间:2008-08-29  
根据用户输入,动态创建查询是很常见的需求,但是Query By Example只能解决简单的属性对比,正如同你说的,between查询或者一个in查询都是很麻烦的。

Query By Criterion是解决这种需求的最好方法,你需要实现的只是一个将用户输入转化成动态创建Criterion的通用方法,能够将map或者list之类容器中的值转化即可:
["eq", "propertyName", "abc"], ["between", "propertyName", "lo", "hi"], ["in", "propertyName", [1, 2, 3]] => Restrictions.eq and Restrictions.between and Restrictions.in

Restrictions只是对各种Criterion实现的提供了静态方法调用,你也可以直接实现一个map/list => criterion的转化方法。
4 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics