NHibernate初学者指南(17):查询的其他知识点
本篇包括以下几个知识点:
- Hibernate查询语言(HQL)
- 延迟加载属性
- 批量执行多个查询
- 预先加载和延迟加载比较
- 批量数据修改
Hibernate查询语言(HQL)
HQL是NHibernate原始的查询语言,它和SQL很像,但是比其他更面向对象。HQL查询定义为字符串,所以不是类型安全的。另一方面,HQL支持动态实体。
每个HQL查询都通过调用ISession接口的CreateQuery方法创建,HQL字符串作为参数。
查询产品列表,如下面的代码所示:
var products = session.CreateQuery("from Product p").List<Product>();
限制查询返回的记录数,可以使用SetMaxResults,跳过一些记录,可以使用SetFirstResult方法。
var first10Products = session.CreateQuery("from Product p") .SetFirstResult(10) .SetMaxResults(10) .List<Product>();
注意从NHibernate3.2开始,我们可以将上面的查询写成"from Product skip 10 take 10"。
筛选产品列表,只检索断货的产品,代码如下所示:
var discontinuedProducts = session .CreateQuery("from Product p where p.Discontinued") .List<Product>();
也可以使用参数定义筛选,代码如下所示:
var hql = "from Product p" + " where p.Category = :category" + " and p.UnitPrice <= :unitPrice"; var cheapFruits = session .CreateQuery(hql) .SetString("category", "Fruits") .SetDecimal("unitPrice", 1.0m) .List<Product>();
注意上面的代码是如何使用SetString和SetDecimal方法定义相应参数值的。SetString和SetDecimal方法的第一个参数是不带冒号的参数名称。
如果想投影实体列表,可以使用前面提到的结果转换器,如下面的代码所示:
var productsLookup = session .CreateQuery("select Id as Id, Name as Name from Product") .SetResultTransformer(Transformers.AliasToBean<NameID>()) .List<NameID>();
注意,必须为每个列定义一个别名,即使别名和列相同。
排序可以使用order by关键字,代码如下所示:
var sortedProducts = session .CreateQuery("from Product p order by p.Name, p.UnitPrice desc") .List<Product>();
如果想根据一个或多个字段分组记录集,可以使用group by关键字。所有出现在select部分且不是根据它分组的都要应用聚合函数,如下面的代码所示:
var productsGrouped = session .CreateQuery("select p.Category as Category," + " count(*) as Count," + " avg(p.UnitPrice) as AveragePrice" + " from Product p" + " group by p.Category") .List();
在上面的代码中,我们根据Category分组,并返回Category和每个category的记录个数以及每个category的平均单价。
由于缺少转换,返回的结果集是IList。每个列表项都是一个object类型的数组。
我们可以定义一个转换,使用LINQ to Object使返回的结果集对开发者更友好:
var productsGrouped = session .CreateQuery("select p.Category as Category," + " count(*) as Count," + " avg(p.UnitPrice) as AveragePrice" + " from Product p" + " group by p.Category") .SetResultTransformer(Transformers.AliasToEntityMap) .List<IDictionary>() .Select(r => new { Category = r["Category"], Count = r["Count"], AveragePrice = r["AveragePrice"], });
AliasToEntityMap转换器转换结果集的每一行类型由object[]为IDictionary,key对应于查询列的别名。使用最后的Select语句,映射IDictionary为带有Category,Count和AveragePrice字段的匿名类型。
延迟加载属性
NHibernate 3的一个新功能就是可以延迟加载一个实体的特定属性。如果实体的某个属性内容非常大,例如图片,这个功能就派上用场了。大多数情况下,操作实体的时候并不需要总是访问这个属性的内容。默认情况下不加载该属性,只有需要时显示的访问它才更有意义。
使用XML定义一个实体映射时,在property标签中有一个lazy特性,如下面的代码所示:
<property name="SomeProperty" lazy="true" …="" />
我们还可以使用Fluent NHibernate的fluent映射API定义延迟属性,如下所示:
Map(x => x.SomeProperty).LazyLoad();
让我们分析一下查询一个带有延迟属性Review的Book实体NHibernate会生成什么SQL语句。这里,我们定义一个简单的Book实体,如下面的代码所示:
public class Book { public virtual int Id { get; set; } public virtual string BookTitle { get; set; } public virtual int YearOfPublication { get; set; } public virtual string Review { get; set; } }
使用Fluent NHibernate映射实体,如下面的代码所示:
public class BookMap : ClassMap<Book> { public BookMap() { Id(x => x.Id); Map(x => x.BookTitle); Map(x => x.YearOfPublication); Map(x => x.Review) .CustomType("StringClob") .LazyLoad(); } }
注意Review属性是如何映射的。我们使用自定义类型StringClob,使用SQL Server时,它会被转换为VARCHAR(MAX)列类型。我们还使用LazyLoad方法定义了Review是延迟属性。
现在根据ID加载实体,NHibernate生成的查询如下图所示:
注意Review列没有出现在上面的SELECT语句中。如果我们现在访问Review属性,那么NHibernate延迟加载这个属性,生成下面的SQL语句:
注意,如果一个实体中有不止一个延迟属性,NHibernate会在第一次访问其中一个属性时加载所有的延迟属性。
批量执行多个查询
到目前为止,我们对数据库进行的每个查询都是往返的。有时候我们需要执行多个查询,为了提高程序的性能,可以批量发送所有的查询到数据库,,然后数据库发送查询结果集的列表给我们,而不是单个结果集。
为此,LINQ to NHibernate提供程序定义了一个ToFuture扩展方法。当访问第一个查询时,由ToFuture终止的所有查询批量发送到数据库。假设我们的程序是订单系统,我们想一次加载类别列表,给定类别的活跃产品的列表,以及返回产品的数量。代码如下所示:
using (var tx = session.BeginTransaction()) { var categories = session.Query<Category>().ToFuture(); var query = session.Query<Product>() .Where(p => !p.Discontinued) .Where(p => p.Category.Name == "Fruits"); var products = query.ToFuture(); var count = query.GroupBy(p => p.Discontinued) .Select(x => x.Count()) .ToFutureValue(); // get the results var result = new Result { Categories = categories.ToArray(), Products = products.ToArray(), ProductCount = count.Value }; } }
由NHibernate生成的批量查询如下图所示:
注意其他的查询API也支持批量查询。
预先加载和延迟加载比较
假设我们有下面的简单模型,如下图所示:
如果已经在数据库中存储了三个person实体,我们执行下面的代码:
var listOfPersons = session.Query<Person>(); foreach (var person in listOfPersons) { Console.WriteLine("{0} {1}", person.LastName, person.FirstName); foreach (var hobby in person.Hobbies) { Console.WriteLine(" {0}", hobby.Name); } }
在NHibernate profiler中的结果如下图所示:
这是典型的select(n+1)问题。NHibernate首先加载所有person列表,然后当访问每个人的爱好时,延迟加载各自爱好的列表。这在大多数情况下都是需要的,尤其是只想访问person实体的属性时或者是访问一部分人的爱好时。
另一方面,如果需要访问所有person的爱好,我们有一种更好的方式加载数据。我们可以告诉NHibernate和person实体一起预先加载所有的爱好。
LINQ to NHibernate中的Fetch方法可以达到这个目的。下面的查询,NHibernate只发送一个语句到数据库:
var persons = session.Query<Person>().Fetch(p => p.Hobbies);
NHibernate生成的SQL如下图所示:
NHibernate使用左外连接一次加载person和爱好。
也可以使用条件查询获得同样的结果,如下面的代码所示:
var persons = session.CreateCriteria<Person>("p") .CreateCriteria("p.Hobbies", JoinType.LeftOuterJoin) .List<Person>();
这里,我们使用CreateCriteria方法定义如何处理独立爱好集合,我们声明了使用左外连接。查询的结果跟使用LINQ提供程序是一样的。
注意我们使用左外连接而不是内连接,因为person可以没有爱好。
最后还可以使用HQL在person和爱好之间使用左外连接操作获得同样的结果,代码如下所示:
var hql = "select p from Person as p left join fetch p.Hobbies as h"; var listOfPersons = session.CreateQuery(hql) .List<Person>();
由NHibernate生成的数据库查询跟LINQ提供程序生成的一样。
批量数据修改
在前面的文章中,我们学习了在NHibernate的帮助下,添加新纪录到数据库,修改、删除存在的记录。这些操作都是基于单个记录的。这是预期的行为,大多数情况下都能满足我们的需求。然而,有时我们想一次对整个数据集合进行修改。NHibernate允许我们使用HQL执行大容量数据的修改。为此,我们可以使用ExecuteUpdate方法,它定义在IQuery接口中。
使用一个查询更新所有产品的单价,代码如下所示:
var hql = "update Product p set p.UnitPrice = 1.1 * p.UnitPrice"; session.CreateQuery(hql).ExecuteUpdate();
NHibernate发送下面的命令到数据库,如下图所示:
批量删除断货的产品,代码如下所示:
var hql = "delete Product p where p.Discontinued=true"; session.CreateQuery(hql).ExecuteUpdate();
最后使用HQL批量插入,代码如下:
var hql = "insert into Product(Id, Name, Category, UnitPrice) " + "select t.Id, t.Name, t.Category, t.UnitPrice " + "from ProductTemp t"; session.CreateQuery(hql).ExecuteUpdate();
批量插入的一些注意事项:
- 数据源必须是数据库的一个(映射表)表。
- 数据源和目标表的所有列必须匹配。
- 不能使用值是由数据库自动生成的Id列。