作者:陈省
对象查询 vs Sql查询
在传统的.Net企业应用的开发是以数据集为核心来进行的,数据集中的数据获取和操作都是通过标准的SQL 语言来实现的,比如通过设定SqlCommand 等组件的CommandText 为相应的Sql 来查询数据的,查询结果通常是强类型或弱类型的DataSet。但是回顾一下我们前面使用的NHibernate来获取业务域对象数据时,我们并没有使用任何的Sql语句,而是使用类似于session.CreateCriteria(typeof(publisher)).List();方法来获取数据的,返回的结果是IList或者IDictionary等保存在集合中的对象实例。
NHibernate中的查询都是面向对象的查询,通过CreateCriteria返回的ICriteria接口来获取对象的面向对象的查询方法叫条件查询,这种查询使用起来比较简单,能够满足基本的查询需要,但是如果用来实现复杂查询的时候,用起来就会比较烦琐。此外,NHibernate提供了一种强大的查询语言(Hibernate Query Language, 以下简称HQL),它的语法非常类似于Sql,但同Sql不同,它是完全面向对象的,具备继承、多态和关联等特性。此外,Java版本的Hibernate还支持通过特定的Sql语句返回面向对象的查询结果,在本文创作时使用的NHibernate 0.2版本中还未实现这一特性。下表是HQL查询语言同标准的Sql语言之间的对比:
HQL查询语言 |
SQL查询语言 |
面向对象的查询语言,“面向对象的SQL”, 强类型的查询语言 |
面向静态数据集的查询语言,非面向对象的 |
数据库平台无关的 |
数据库平台相关,不同平台的SQL 语言用法 不同 |
只能用于对数据进行查询操作,不能用于数据增、删、改。 |
除了查询,可以通过Insert,Update , Delete 对数据进行修改 |
模型映射
接下来,我们来演示如果使用NHibernate实现基本的数据查询。出于简单的考虑,我们使用Sql Server默认安装的pubs示例数据库(对应于一个出版信息系统的信息模型),这样可以减少建表和测试数据录入的工作。从pubs库中,选择employee和pulishers表作为数据查询对象,两者的关系是一对多的关系,如下图示意:
其中pubishers表保存所有的出版公司信息,每个出版实体具有Id, 名称(pub_name),所在城市、州、国家的属性。Employee表保存出版公司雇员的信息,每个雇员实体具有Id(emp_id),姓(fname), 中间缩写(minit),名(lname),对应的公司标识(pub_id),雇佣时间、职位等等属性信息。
要想让NHibernate能够查询这些实体信息,首先需要根据DbSchema建立映射文件及其对应的类定义。新建一个项目,定义如下的类:
/// <summary>
/// publisher对应于出版商类
/// </summary>
public class publisher
{
public publisher()
{
}
//出版商的ID
private string _PubId;
public string PubId
{
get {return _PubId;}
set {_PubId=value;}
}
//出版商名称
private string _PubName;
public string PubName
{
get {return _PubName;}
set {_PubName=value;}
}
//城市
private string _City;
public string City
{
get{return _City;}
set{_City=value;}
}
//所在州
private string _State;
public string State
{
get {return _State;}
set {_State=value;}
}
//国家
private string _Country;
public string Country
{
get {return _Country;}
set {_Country=value;}
}
//雇员列表
private IDictionary _Employee;
public IDictionary Employee
{
get { return _Employee; }
set { _Employee = value; }
}
}
/// <summary>
/// employee对应于雇员类
/// </summary>
public class employee
{
public employee()
{
}
//雇员号
private string _EmpId;
public string EmpId
{
get { return _EmpId; }
set { _EmpId = value; }
}
//姓
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set { _FirstName = value; }
}
//中间名
private string _MiddleName;
public string MiddleName
{
get { return _MiddleName; }
set { _MiddleName = value; }
}
//名
private string _LastName;
public string LastName
{
get { return _LastName; }
set { _LastName = value; }
}
//雇佣时间
private DateTime _HireDate;
public System.DateTime HireDate
{
get { return _HireDate; }
set { _HireDate = value; }
}
//对应的出版商
private publisher _pub;
public NHQuery.publisher pub
{
get { return _pub; }
set { _pub = value; }
}
}
建立的映射文件pubs.hbm.xml内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class name="NHQuery.publisher, NHQuery" table="publishers">
<id name="PubId" column="pub_id" type="String" length="4">
<generator class="assigned" />
</id>
<set name="Employee" table="employee" cascade="all" inverse="true">
<key column="pub_id" />
<one-to-many class="NHQuery.employee, NHQuery" />
</set>
<property name="PubName" column="pub_name" type="String" length="40"/>
<property name="City" column="city" type="String" length="20"/>
<property name="State" column="state" type="String" length="2"/>
<property name="Country" column="country" type="String" length="30"/>
</class>
<class name="NHQuery.employee, NHQuery" table="employee">
<id name="EmpId" column="emp_id" type="String" length="9">
<generator class="assigned" />
</id>
<property name="FirstName" column="fname" type="String" length="20"/>
<property name="MiddleName" column="minit" type="String" length="1"/>
<property name="LastName" column="lname" type="String" length="30"/>
<many-to-one name="pub" column="pub_id" class="NHQuery.publisher, NHQuery"/>
<property name="HireDate" column="hire_date" type="DateTime"/>
</class>
</hibernate-mapping>
表示层设计
界面示意图如下:
界面包括两部分,一部分用于显示条件查询的结果,一部分显示HQL查询结果,结果用DataGrid来展示,对于HQL查询来说,提供了一个输入框,用来输入HQL查询语句。
业务逻辑的实现
条件查询
假设我们现在要获取所有国家为USA的出版商,则可以通过调用CreateCriteria方法获得ICriteria接口,然后添加一个表达式对象Expression,示例如下:
ICriteria criteria=session.CreateCriteria(typeof(publisher));
criteria.Add(Expression.Eq("Country","
上面的语句对应的NHibernate运行时生成的Sql语句如下:
declare @P1 int
set @P1=3
exec sp_prepexec @P1 output, N'@p0 nvarchar(4000)', N'SELECT publishe0_.pub_id as pub_id, publishe0_.pub_name as pub_name, publishe0_.city as city, publishe0_.state as state, publishe0_.country as country FROM publishers publishe0_ WHERE (publishe0_.country = @p0)', @p0 = N'USA'
select @P1
可以注意到,NHibernate生成的Sql语句是非常智能的,性能是尽可能优化的,比如特别生成了一个针对出版商国家属性的参数化查询,而不是一条简单的Select * from publishers where country=’USA’。生成参数化查询的好处就是很多大型数据库会把参数化查询在编译后缓存起来,如果后面反复调用的话,则数据库不再重新编译,可以直接调用,对于查询性能有很大的优化。
NHibernate的条件查询表达式类Expression除了支持使用Eq实现等于条件查询外,还支持gt(大于条件),ge(大于等于),lt(小于),le(小于等于),Between(对应于Sql的Between),like(对应于Sql 的Like表达式), in(对应于Sql的in表达式),and/or(对应于Sql 的and/or逻辑运算)等等常见的查询条件,要想查询同时满足多个查询条件的数据,只要多次调用Add方法添加多个查询对象即可。
最为灵活的是,NHibernate的查询对象还提供了原生Sql语法的支持,比如下面的语句就把所有pub_name以A字母打头的数据查询出来。
criteria.Add(Expression.Sql("pub_name like 'A%'"));
除了支持类似于Sql中where的查询条件外,NHibernate的条件查询还支持对应于Sql的Order的排序功能,比如我们要想按照出版商的PubName属性按降序输出结果时,可以调用AddOrder方法来实现,代码如下:
criteria.AddOrder(Order.Desc("PubName"));
另外,条件查询还提供了简单的分页支持,我们可以利用分页支持限定返回记录的范围。比如要想从第二条记录开始,返回最多3条记录的话,可以用下面代码实现:
criteria.SetFirstResult(2);
criteria.SetMaxResults(3);
不过需要注意的一点,通过跟踪我们会发现,调用上面两个方法对查询结果范围做限制并不会改变NHibernate运行时所产生的Sql语句,因此可以看出条件查询对于分页的支持比较简单,没有对性能做优化。因此需要审慎的使用。
HQL查询
HQL查询是通过session的CreateQuery方法返回的IQuery接口来实现的,代码示意如下:
private void btnHql_Click(object sender, System.EventArgs e)
{
//Hql查询
try
{
ISession session=factory.OpenSession();
IQuery query=session.CreateQuery(this.rtbHql.Text.Trim());
this.dgHql.DataSource=null;
this.dgHql.DataSource=query.List();
session.Close();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
可以在界面上的文本输入框中输入HQL语句,运行后检验HQL的查询结果。比如,要想用HQL获取所有国家为USA的出版商,可以输入下面的HQL语句:
from publisher where Country='
要注意的是HQL语句中查询目标的类名和属性名称是区分大小写的,比如publisher不能写成Publisher,但是目前NHibernate的实现有点问题,在某些情况下对象的属性值是不区别大小写的,需要注意。
同样的,HQL也支持排序的语法,如果要按照出版社的名称排序输出结果的话,可以输入如下的查询语句:
from publisher as pub order by pub.PubName
HQL支持对象别名,上面语句中的pub就是publisher对象的别名。此外,HQL还支持group by , having子句,子查询等特性,这使得HQL也适合用于生成各种复杂报表。
HQL统计函数
很多时候,我们可能并不关心数据对象本身,可能更关心对一组相关对象的统计结果,HQL同样支持类似于Sql的Count, Sum, Avg等聚集函数的简单统计查询功能,比如想要查询国家为USA的出版社的数量,可以输入下面的查询语句:
select count(*) from publisher where Country='
统计结果的展示同普通的查询不同,返回的不是一个集合,因此需要通过IQuery的Enumerable接口通过转型获取,代码如下:
private void btnValue_Click(object sender, System.EventArgs e)
{
try
{
ISession session=factory.OpenSession();
IQuery query=session.CreateQuery(this.rtbHql.Text.Trim());
IEnumerator itor=query.Enumerable().GetEnumerator();
itor.MoveNext();
MessageBox.Show(itor.Current.ToString());
session.Close();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
HQL关联查询
上面举的HQL的例子都是些单表的查询,实际应用中碰到的更多的查询是关联查询,HQL支持内连接、左联接、右连接和全连接,比如要想通过查询获取关联的雇员的名字及其工作的出版社的名称,采用Sql语句来实现的话,需要输入下面的语句:
select emp.fname, emp.minit, emp.lname, pub.pub_name from employee as emp, publishers as pub where emp.pub_id=pub.pub_id
其对应的HQL语句示意如下:
select emp.FirstName, emp.MiddleName, emp.LastName, pub.PubName from employee as emp inner join emp.pub as pub
注意在HQL语句中,我们无须写Where子句来定义关联,这是因为HQL会自动分析到我们在hbm.xml中定义的1对多的关联,自动建立关联。运行上面的HQL,你会发现DataGrid显示的网格会显示一个怪异的列表,见下图示意:
这显然不是我们想要的效果,Debug分析一下IQuery.List()方法的返回结果,你会发现只有当HQL返回publisher或者employee对象时,List()方法会返回publisher或者employee对象的ArrayList集合,但是如果只选择publisher等对象的几个属性时,返回的ArrayList中的元素是属性值数组,绑定到DataGird上的效果就不对了,那么如何解决这个问题呢?
幸好HQL提供了根据返回的属性值,构造一个新对象集合的功能,假设有一个EmpJoinPub类,它的构造函数可以接受FirstName, MiddleName, LastName,PubName作为构造参数,则我们可以把HQL改写成下面的形式:
select new EmpJoinPub(emp.FirstName, emp.MiddleName, emp.LastName, pub.PubName) from employee as emp inner join emp.pub as pub
这回HQL会返回一个EmpJoinPub对象集合,这样绑定到DataGrid的效果就是我们想要的了。下面就是EmpJoinPub类的定义:
public class EmpJoinPub
{
public EmpJoinPub(string FirstName, string MiddleName, string LastName, string PubName)
{
this._EmpName=string.Format("{0}.{1}.{2}", FirstName, MiddleName, LastName);
this._Publisher=PubName;
}
//出版商名称
private string _Publisher;
public string Publisher
{
get { return _Publisher; }
set { _Publisher = value; }
}
//雇员名称
private string _EmpName;
public string EmpName
{
get { return _EmpName; }
set { _EmpName = value; }
}
}
为了要让NHibernate知道到什么地方去找这个EmpJoinPub类,我们需要将EmpJoinPub类也导入到hbm.xml 文件定义中,在pubs.hbm.xml中添加下面的语句就可以了:
<import class="NHQuery.EmpJoinPub, NHQuery"/>
运行新的HQL语句,显示的效果如下:
总结
在本文中,我们探讨了NHibenrate强大的面向对象的查询语言,但是要说明的是NHibernate的数据查询语言是有其适用范围的,比如它无法用于数据的批量删除和修改。如果需要对数据进行大量修改操作的话,Hibernate作者的建议是不考虑使用NHibernate来实现,或者批量修改基于普通SQL来完成。