首先,把PlantItem对象的数据结构做一些调整,然后再进行本节中的测试。 在NHibernate考察系列 04一节中测试结果,象PlantItem这种复合主键对象,使用一个语意上的ID比较合适,这里我们就
在NHibernate考察系列 04一节中测试结果,象PlantItem这种复合主键对象,使用一个语意上的ID比较合适,这里我们就按照这种方式修改过来。因为domain对ID属性没有任何依赖,不用于对象间的关联,因此使用一个整数类型就可以了。为TBLPLANTITEM表添加一个int的ID字段,设置成identity(最好为ID字段添加一个非聚集索引)。类和映射文件大致如下:
public int ID
{
get { return _id; }
set { _id = value; }
}
private int _id;
public virtual Plant Plant
{
get { return _plant; }
set { _plant = value; }
}
private Plant _plant;
public virtual Item Item
{
get { return _item; }
set { _item = value; }
}
private Item _item;
<id name="ID">
<column name="ID" sql-type="int"/>
<generator class="native" />
</id>
<many-to-one name="Plant" class="Plant" column="PLANT_ID" lazy="proxy"/>
<many-to-one name="Item" class="Item" column="ITEM_ID" lazy="proxy"/> 用下面的测试代码加入测试数据:
Company company = new Company("1000", "test company 1", "", new HashedSet<Plant>());
session.Save(company);
Plant plant1 = new Plant("1105", "test plant 1", company);
session.Save(plant1);
Plant plant2 = new Plant("1202", "test plant 2", company);
session.Save(plant2);
Item item1 = new Item("FK1.1023.78AF", "2.5# LCD", "PCS", new decimal(85.7));
session.Save(item1);
Item item2 = new Item("191.1023.78AF", "test item 2", "PCS", new decimal(12));
session.Save(item2);
Item item3 = new Item("023.0000.1233", "test item 3", "PCS", new decimal(25.7));
session.Save(item3);
Item item4 = new Item("FK1.2314.ZF31", "test item 4", "PCS", new decimal(1.789));
session.Save(item4);
Item item5 = new Item("1000 0000 0070", "test item 5", "EA", new decimal(19));
session.Save(item5);
Item item6 = new Item("ANTXX00230GD", "test item 6", "EA", new decimal(19));
session.Save(item6);
//创建PlantItem对象
session.Save(new PlantItem(plant1, item1, "PCS", ItemCategoryEnum.P, PurchaseCategoryEnum.JIT, StockOptionEnum.ERP));
session.Save(new PlantItem(plant1, item2, "PCS", ItemCategoryEnum.P, PurchaseCategoryEnum.PO, StockOptionEnum.None));
session.Save(new PlantItem(plant1, item3, "PCS", ItemCategoryEnum.P, PurchaseCategoryEnum.PO, StockOptionEnum.ERP));
session.Save(new PlantItem(plant1, item4, "PCS", ItemCategoryEnum.M, PurchaseCategoryEnum.JIT, StockOptionEnum.Hub));
session.Save(new PlantItem(plant1, item5, "PCS1", ItemCategoryEnum.M, PurchaseCategoryEnum.JIT, StockOptionEnum.None));
session.Save(new PlantItem(plant1, item6, "EA", ItemCategoryEnum.M, PurchaseCategoryEnum.PO, StockOptionEnum.None));
session.Save(new PlantItem(plant2, item2, "PCS", ItemCategoryEnum.M, PurchaseCategoryEnum.JIT, StockOptionEnum.ERP));
session.Save(new PlantItem(plant2, item3, "PCS", ItemCategoryEnum.M, PurchaseCategoryEnum.PO, StockOptionEnum.Hub));
session.Save(new PlantItem(plant2, item4, "PCS", ItemCategoryEnum.P, PurchaseCategoryEnum.PO, StockOptionEnum.Hub));
session.Save(new PlantItem(plant2, item5, "PCS", ItemCategoryEnum.M, PurchaseCategoryEnum.PO, StockOptionEnum.Hub));
1. Criteria 条件式查询
ICriteria criteria = session.CreateCriteria(typeof(PlantItem));
criteria.Add(Expression.Eq("PurchaseCategory", PurchaseCategoryEnum.PO))
.AddOrder(Order.Desc("Item"))
.SetFirstResult(2).SetMaxResults(2);
IList<PlantItem> list = criteria.List<PlantItem>();
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine("{0}\t{1}\t\t{2}",
list[i].Plant.PlantID, list[i].Item.ItemID, list[i].PurchaseCategory.ToString());
} SQL语句:
exec sp_executesql N'
SELECT top 4 this_.ID as ID2_0_, this_.PLANT_ID as PLANT2_2_0_, this_.ITEM_ID as ITEM3_2_0_,
this_.UNIT as UNIT2_0_, this_.ITEM_CATEGORY as ITEM5_2_0_, this_.PURCHASE_CATEGORY as PURCHASE6_2_0_,
this_.STOCK_OPTION as STOCK7_2_0_, this_.CREATE_DATE as CREATE8_2_0_, this_.CREATE_TIME as CREATE9_2_0_
FROM TBLPLANTITEM this_ WHERE this_.PURCHASE_CATEGORY = @p0 ORDER BY this_.ITEM_ID desc',
N'@p0 nvarchar(2)', @p0 = N'PO' 条件式查询提供like、in、between等各种方式查询,基本可以满足应用的要求。
条件式查询以及HQL都是在对象层面进行query,因此提供的参数都是对象属性名称、属性值。例如上面的AddOrder语句,是要求结果集按照PlantItem对象的Item属性降序排列,NHibernate根据映射信息生成SQL,在SQL中使用的是ORDER BY ITEM_ID desc实现。
SetFirstResult、SetMaxResults可以用于实现分页,不同的数据库有不同的实现方式,例如SQL Server 2000使用的是上面取top 4的方式,然后在程序中过滤掉FirstResult之前的对象。
如果映射中配置了对象关联,可以在条件式查询中使用关联进行查询:
ICriteria criteria = session.CreateCriteria(typeof(PlantItem))
.Add(Expression.Eq("PurchaseCategory", PurchaseCategoryEnum.PO));
ICriteria itemCriteria = criteria.CreateCriteria("Item")
.Add(Expression.Like("ItemDescription", "%test item%"))
.AddOrder(Order.Desc("ItemDescription"));
IList<PlantItem> list = criteria.List<PlantItem>();
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine("{0}\t{1}\t\t{2}\t{3}",
list[i].Plant.PlantID, list[i].Item.ItemID, list[i].PurchaseCategory.ToString(), list[i].Item.ItemDescription);
} SQL语句:
exec sp_executesql N'
SELECT this_.ID as ID2_1_, this_.PLANT_ID as PLANT2_2_1_, this_.ITEM_ID as ITEM3_2_1_, this_.UNIT as UNIT2_1_,
this_.ITEM_CATEGORY as ITEM5_2_1_, this_.PURCHASE_CATEGORY as PURCHASE6_2_1_,
this_.STOCK_OPTION as STOCK7_2_1_, this_.CREATE_DATE as CREATE8_2_1_, this_.CREATE_TIME as CREATE9_2_1_,
item1_.ITEM_ID as ITEM1_7_0_, item1_.ITEM_DESCRIPTION as ITEM2_7_0_, item1_.UNIT as UNIT7_0_,
item1_.PRICE as PRICE7_0_
FROM TBLPLANTITEM this_ inner join TBLITEM item1_ on this_.ITEM_ID=item1_.ITEM_ID
WHERE this_.PURCHASE_CATEGORY = @p0 and item1_.ITEM_DESCRIPTION like @p1
ORDER BY item1_.ITEM_DESCRIPTION desc',
N'@p0 nvarchar(2),@p1 nvarchar(11)',
@p0 = N'PO', @p1 = N'%test item%' 上面的语句查询PlantItem集合对象,对PurchaseCategory下条件,也对PlantItem.Item.ItemDescription属性下条件,并要求按照PlantItem.Item.ItemDescription降序排列。NHibernate根据映射配置文件中PlantItem类与Item类之间的many-to-one关系,自动将TBLPLANTITEM与TBLITEM进行inner join查询,然后对ITEM_DESCRIPTION字段下条件,按照ITEM_DESCRIPTION排序。
另外一种条件式查询方式,是给出一个example对象,NHibernate根据传入的example对象属性是否有赋值,确定是否应该对该属性生成一个查询条件。
2. HQL
HQL语法上类似SQL,也不同于SQL。SQL面向的是数据库的database schema/data structure,HQL面向的是对象模型。
简单用法如下:
Plant plant=session.Get<Plant>("1105");
string hql = "from NH12.MyExample.Domain.PlantItem as pi where pi.Item.ItemID=:ItemID and pi.Plant=:Plant";
IQuery query = session.CreateQuery(hql)
.SetString("ItemID", "191.1023.78AF")
.SetEntity("Plant", plant);
IList<PlantItem> list = query.List<PlantItem>(); 首先,整个hql语句中使用的都是对象、对象属性,跟数据库table没有关系。
上面的hql要查询的是PlantItem对象列表,因为是基于对象模型,映射配置文件中PlantItem、Plant、Item对象之间的关联关系已经配置,因此在hql中可以使用这些对象以及它们之间已经建立的关联关系。既可以象ItemID一样使用Item对象的属性进行查询,也可以象Plant一样,对整个Plant属性下查询条件。SQL如下:
exec sp_executesql N'
select plantitem0_.ID as ID2_, plantitem0_.PLANT_ID as PLANT2_2_, plantitem0_.ITEM_ID as ITEM3_2_,
plantitem0_.UNIT as UNIT2_, plantitem0_.ITEM_CATEGORY as ITEM5_2_,
plantitem0_.PURCHASE_CATEGORY as PURCHASE6_2_, plantitem0_.STOCK_OPTION as STOCK7_2_,
plantitem0_.CREATE_DATE as CREATE8_2_, plantitem0_.CREATE_TIME as CREATE9_2_
from TBLPLANTITEM plantitem0_
where (plantitem0_.ITEM_ID=@p0 )and(plantitem0_.PLANT_ID=@p1 )',
N'@p0 nvarchar(13),@p1 nvarchar(4)',
@p0 = N'191.1023.78AF', @p1 = N'1105' 如果是对整个对象下查询条件,NHibernate根据关联的ID生成SQL条件;如果对关联对象仅仅使用到关联用的ID字段,NHB会比较智能,不会使用join。例如上面的例子,虽然对PlantItem.Item.ItemID加了一个查询条件,但NHB并没有对这两个表使用join子句。如果上面的例子中添加一个查询条件:pi.Item.ItemDescription like :ItemDesc,这样NHB就必须要将TBLPLANTITEM和TBLITEM表join进行查询了。
hql也可以象SQL多个表join一样,对于那些在映射配置文件中并没有建立的关联关系,可以在hql中使用join。例如:
from PlantItem as pi, User usr where pi.Plant.PlantID=usr.UserID
接下来我对一件事情比较感兴趣。在NHibernate考察系列 04中,PlantItem对象加了个CreateTime属性,这个属性有点特殊,在对象中使用的是一个DateTime类型的属性,而映射到数据库却是将日期和时间分别转化成字符串保存在数据库的CREATE_DATE、CREATE_TIME两个字段中。这种情况下,如果需要在hql中对这个属性下查询条件,情况会怎么样?
string hql = "from NH12.MyExample.Domain.PlantItem as pi where pi.CreateTime>:Time";
IQuery query = session.CreateQuery(hql)
.SetParameter("Time", DateTime.Parse("2007-04-14 16:01:01"));
IList<PlantItem> list = query.List<PlantItem>(); 测试结果,NHibernate不支持,异常信息为:path expression ends in a composite value。Hibernate能够实现这一特性将会是一件非常好的事情,但要实现这一特性不简单。以上面的例子来看,Hibernate怎样根据CreateTime>:Time这一条件生成SQL?((CREATE_DATE=@date AND CREATE_TIME>@time) OR CREATE_DATE>@date)?在不同的运用场景下情况会更复杂。
这里对上面这种情形的用户自定义类型带来相当的限制,因此慎用。
详细的hql语法参考NHibernate的文档。
个人对条件式查询、hql的看法:
1. 实现多数据库兼容。
2. 对持久化存取知识的封装。可能比较直观的想法是既然做到了多数据库兼容,数据库知识的封装作用已经不大。一方面,对象属性与持久化存储之间有差异,使用hql使得这种差异被封装在映射文件或者自定义的映射类型中。另一方面,在粗粒度对象设计的情况下,实体跟表之间基本一一对应,在对象模型层面写hql跟在table层面写SQL的确没有太多差异。但如果使用细粒度对象设计,对象模型与table之间的差异就会相当明显,domain既要处理映射行为,控制数据存取,又要处理复杂的对象关系,情况就复杂化了。分离一个DAL出来,成本代价也是比较高。
3. 性能问题。
这个问题就有点复杂化了。使用ORM之后,在应用与数据库之间建立一个隔离带,性能问题不会消失。解决性能问题有两种倾向,第一种是类似iBATIS做法,把存取数据用的SQL做分离,集中管理,方便数据库、SQL层面做优化。另一种做法是结合domain的架构设计来解决性能问题。
NHibernate的应用产生性能问题,一方面是不合理的对象关系,在数据加载方面造成浪费、开销。另一方面是不合理的条件式查询、hql的使用。确保良好的对象模型设计,根据设计思想,在条件式查询、hql的使用上制定约束、规范变得很重要。
3. Named Query
Named Query是将hql从代码中分离出来,以命名的形式放入配置文件中。这跟iBATIS的方式有点类似,便于对hql的集中管理和维护。
在PlantItem.hbm.xml配置文件的hibernate-mapping元素下添加下面的Named Query hql语句:
<query name="NH12.MyExample.Domain.PlantItemQuery">
<![CDATA[
from NH12.MyExample.Domain.PlantItem as pi
where pi.Item.ItemDescription like :ItemDesc
order by pi.Plant, pi.Item
]]>
</query> 程序中使用:
IList<PlantItem> list = session.GetNamedQuery("NH12.MyExample.Domain.PlantItemQuery")
.SetString("ItemDesc", "test item 5%")
.List<PlantItem>();
4. Native SQL
Native SQL提供直接对数据库访问的机会。
string sql=@"
select pi.PLANT_ID as PlantID,pi.ITEM_ID as ItemID,i.ITEM_DESCRIPTION as ItemDescription
from TBLPLANTITEM pi
inner join TBLITEM i on i.ITEM_ID=pi.ITEM_ID
order by pi.PLANT_ID, i.ITEM_DESCRIPTION
";
ISQLQuery query = session.CreateSQLQuery(sql)
.AddScalar("PlantID", NHibernateUtil.String)
.AddScalar("ItemID", NHibernateUtil.String)
.AddScalar("ItemDescription", NHibernateUtil.String);
IList list = query.List(); 返回的是一个object数组的列表。
用下面的方法返回实体对象列表:
string sql = @"select * from TBLPLANTITEM";
ISQLQuery query = session.CreateSQLQuery(sql).AddEntity(typeof(PlantItem));
IList<PlantItem> list = query.List<PlantItem>();
用下面的方法可以一次返回多个对象列表:
string sql = @"select {pi.*}, {i.*} from TBLPLANTITEM pi, TBLITEM i where pi.ITEM_ID=i.ITEM_ID";
ISQLQuery query = session.CreateSQLQuery(sql)
.AddEntity("pi", typeof(PlantItem))
.AddEntity("i", typeof(Item));
IList list = query.List();
for (int i = 0; i < list.Count; i++)
{
object[] collections = list[i] as object[];
PlantItem pi = collections[0] as PlantItem;
Item item = collections[1] as Item;
Console.WriteLine("{0},\t{1},\t{2},\t{3}", pi.Plant.PlantID, pi.Item.ItemID, pi.Item.ItemDescription, pi.Plant.PlantName);
Console.WriteLine("{0},\t{1}", item.ItemID, item.ItemDescription);
} 首先注意SQL语句,对需要选择的列必须使用别名,因为两个表中有一个名称相同的字段ITEM_ID。SQL语句中的{pi.*}, {i.*}不是SQL语法,而是告诉NHibernate对pi.*和i.*中的所有列都使用别名,以区别选出的每一个字段是属于哪个对象的。数据库执行的SQL如下,可以看到跟我们在代码中给出的SQL已经不一样了,主要是NHB在中间进行了处理,为每个列生成了别名:
select pi.ID as ID2_0_, pi.PLANT_ID as PLANT2_2_0_, pi.ITEM_ID as ITEM3_2_0_, pi.UNIT as UNIT2_0_,
pi.ITEM_CATEGORY as ITEM5_2_0_, pi.PURCHASE_CATEGORY as PURCHASE6_2_0_,
pi.STOCK_OPTION as STOCK7_2_0_, pi.CREATE_DATE as CREATE8_2_0_, pi.CREATE_TIME as CREATE9_2_0_,
i.ITEM_ID as ITEM1_7_1_, i.ITEM_DESCRIPTION as ITEM2_7_1_, i.UNIT as UNIT7_1_, i.PRICE as PRICE7_1_
from TBLPLANTITEM pi, TBLITEM i
where pi.ITEM_ID=i.ITEM_ID 返回的IList对象是一个object[]类型,第一个元素是PlantItem对象,第二个元素是Item对象。
单步调试上面的测试代码,并监控SQL语句,可以发现另外一个小区别:访问pi.Plant.PlantName可能会产生一个SQL查询,因为相关的Plant对象可能还没有被缓存到session中;而任何时候访问pi.Item.ItemDescription,都不会再产生SQL查询,因为在执行IList list = query.List()时相关Item对象已经一起返回,并被session缓存过了。
上面这些都是根据NHibernate的文档写的测试代码,下面的一些功能特性在文档中都讲述的比较详细,也就不再测试了。
Named SQL Query,跟NamedQuery差不多,但是需要提供另外的一些信息,例如返回的各个字段Type是什么,是否返回某一个实体对象,如果是,返回的结果集中各个字段跟实体属性的对应关系等。
可以使用存储过程,跟Named SQL Query一样,需要对返回的结果集进行额外的一些配置说明。如果存储过程返回多个DataTable(SQL Server、MySQL等),NHB只取第一个DataTable。
可以为实体的insert、update、delete使用自定义的SQL语句,可以为实体的load使用hql或者是SQL的Named Query,可以在这些里面使用存储过程。
Native SQL的支持,包括实体的insert、update、delete、load中对SQL的支持,是Hibernate留给用户解决复杂问题的最后手段了。使用Native SQL解决问题的灵活度相当大,也能使你的应用架构很好的跟NHibernate整合在一起,因此适当的使用Native SQL、NHibernate提供的扩展,在设计上会拥有相当不错的发挥空间。