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也支持批量查询。

预先加载和延迟加载比较

假设我们有下面的简单模型,如下图所示:

image

如果已经在数据库中存储了三个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列。
posted @ 2011-11-28 10:53  BobTian  阅读(2963)  评论(0编辑  收藏  举报