NHibernate初学者指南(15):使用LINQ to NHibernate提供程序查询数据
在前面的《NHibernate初学者指南(8):增删查改》一文中简单的提到了查询一个实体的Get<T>和Load<T>方法以及查询实体列表的Query<T>方法,这篇文章我们进一步讲解查询的有关方面。
理论知识
限制返回的记录数
var orders = session.Query<Order>();
上面的查询返回Order表中所有的订单,如果想限制返回记录的个数,可以使用Take方法,如下面的代码:
var orders = session.Query<Order>().Take(200);
筛选记录集
使用筛选的关键字是Where。LINQ定义了一个Where方法,它将一个谓词作为参数。谓词是带有一个参数的函数它返回一个布尔值,如下面的代码所示:
Func<T, bool> predicate
如果要查询所有下架的产品,可以使用下面的代码:
var discontinuedProducts = session .Query<Product>() .Where(p => p.Discontinued);
上面的查询中,我们使用一个拉姆达表达式p=>p.Discontinued定义谓词。这个表达式的意思是:给定一个产品p,获取它的Discontinued属性的内容并返回。如果返回的值是true,那么相应的产品就包括在筛选的结果集中。
映射记录集
映射记录集可以使用Select方法,它将一个映射函数作为参数。映射函数定义为如下代码:
Func<TSource, TResult> mapper
如果我们想加载下架的产品列表,但是只有产品的Id和Name。我们可以先定义一个类NameID,如下所示:
public class NameID { public int Id { get; set; } public string Name { get; set; } }
查询如下所示:
var discontinuedProducts = session .Query<Product>() .Where(p => p.Discontinued) .Select(p => new NameID { Id = p.Id, Name = p.Name });
上面的代码我们定义了映射函数作为拉姆达表达式,如下面的代码:
p => new NameID { Id = p.Id, Name = p.Name }
它的意思是接受一个Product对象p并返回一个NameID类型的对象,它的属性为:Id和Name,对应于产品p的Id和Name。
排序结果集
假设我们想获得Person对象的列表,按姓氏排序,如下面的代码所示:
var people = session.Query<Person>() .OrderBy(p => p.LastName);
如果我们还想按照first name排序,那么查询如下所示:
var people = session.Query<Person>() .OrderBy(p => p.LastName) .ThenBy(p => p.FirstName);
注意我们可以按照多个列排序,只需要简单的在后面调用ThenBy方法即可。还有与OrderBy和ThenBy相对应的是按照降序排列的方法:OrderByDescending和ThenByDescending。上面的查询按照相反的顺序排列,如下面的代码所示:
var people = session.Query<Person>() .OrderByDescending(p => p.LastName) .ThenByDescending(p => p.FirstName);
分组记录
如果我们想计算有多少个人的姓氏以相同的字母开始,那么我们可以借助于分组函数,代码如下所示:
var personsPerLetter = session.Query<Person>() .GroupBy(p => p.LastName.Substring(0, 1)) .Select(g => new { Letter = g.Key, Count = g.Count() });
上面的代码返回匿名类型的列表,它包含想要的结果。
GroupBy方法返回IEnumerable<IGrouping<T,V>>,T表示分组根据的值的类型,V表示被分组的类型。在上面的代码中,T是string类型的,V是Person类型的。
强制LINQ查询立即执行
LINQ查询总是延迟执行的,只有当我们开始遍历结果集时查询才真正的执行。但是有时,我们想强制执行查询并返回结果集。有不同的方式可以这样做。
第一,我们可以调用扩展方法ToArray或ToList终止查询。这两个方法立即开始枚举结果集并将结果对象放到数组或列表中。
var products = session.Query<Product>() .Where(p => p.ReorderLevel > p.UnitsOnStock) .ToArray();
上面的查询加载所有需要再次订货的所有产品列表,并将它们放入到一个数组中。
第二,使用ToDictionary方法。这个方法在从数据库中检索出实体列表并为每一个实例使用唯一键存储在集合中非常方便。我们看一个例子,如下面的代码所示:
var personsPerLetter = session.Query<Person>() .GroupBy(p => p.LastName.Substring(0, 1)) .Select(g => new { Letter = g.Key, Count = g.Count() }) .ToDictionary(x => x.Letter, x => x.Count);
上面的查询创建了一个人数的字典,它的姓氏以给定的字母开头。key是姓氏的开头字母,value是人数。
从查询数据库改为查询内存对象
又是,我们想从数据库查询一些数据,然后进一步使用底层数据库不支持的功能操作这些数据。这种情况,我们需要一种方式通知LINQ to NHibernate提供程序从现在起所有的操作都在内存中完成而不是数据库中。为此,我们可以使用AsEnumerable方法。
让我们看一个例子。假如有一个Email实体,它包含一些记录。现在,我们想生成一个邮件的筛选列表,筛选条件是一个正则表达式。然而,大多数数据库不支持正则表达式。因此,筛选必须在内存中完成,如下面的代码所示:
var statusList = session.Query<Email>() .Select(e => e.EmailAddress) .AsEnumerable() .Where(e => SomeFilterFunctionUsingRegExp(e));
上面的例子,投影发生在数据库,而筛选在内存中完成。
使用LINQ to NHibernate创建报表
为了熟悉LINQ to NHibernate的使用,我们创建几个不同的报表打印到屏幕上。在下面的例子中,我们的模型是天文学的一部分,更确切的说是星体分类。我们使用XML配置文件和XML映射文件映射我们的模型。
1. 在SSMS中创建一个空的数据库:LinqToNHibernateSample。
2. 在Visual Studio中创建一个控制台应用程序:LinqToNHibernateSample。
3. 设置项目的Target Framework为.NET Framework 4。
4. 添加对NHibernate和NHibernate.ByteCode.Castle程序集的引用。
5. 在解决方案中添加一个解决方案文件夹:Schema,并添加nhibernate-configuration.xsd 和nhibernatemapping.
xsd两个文件。
6. 在项目中添加一个类文件Star.cs,添加如下代码定义Star实体:
public class Star { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual IList<Planet> Planets { get; set; } public virtual StarTypes Class { get; set; } public virtual SurfaceColor Color { get; set; } public virtual double Mass { get; set; } }
7. 在项目中添加一个类文件Planet.cs,添加如下代码定义Planet实体:
public class Planet { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual bool IsHabitable { get; set; } public virtual Star Sun { get; set; } }
8. 在项目中添加一个类文件SurfaceColor.cs,在文件中定义枚举SurfaceColor,代码如下所示:
public enum SurfaceColor { Blue, BueToWhite, WhiteToYellow, OrangeToRed, Red }
9. 在项目中添加一个类文件StarTypes.cs,在文件中定义枚举StarTypes,代码如下所示:
public enum StarTypes { O, B, A, F, G, K, M }
10. 在项目中添加一个XML文件Star.hbm.xml,设置它的Build Action属性为Embedded Resource,并添加如下代码定义Star实体的映射:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="LinqToNHibernateSample" namespace="LinqToNHibernateSample"> <class name="Star"> <id name="Id"> <generator class="guid.comb"/> </id> <property name="Name"/> <property name="Mass"/> <property name="Class" type="StarTypes"/> <property name="Color" type="SurfaceColor"/> <bag name="Planets" inverse="true" cascade="all-delete-orphan"> <key column="StarId" /> <one-to-many class="Planet"/> </bag> </class> </hibernate-mapping>
注意Star实体的两个枚举类型属性Color和Class是如何映射的。我们使用type特性指定NHibernate映射这些属性为数据库的int类型的列(枚举的基类型是int)。另外注意一个star的planets列表是如何映射的。尤其复习一下inverse和cascade特性以及集合类型的使用。还要注意的是我们使用Guid生成器。
11. 在项目中添加另一个XML文件Planet.hbm.xml,设置它的Action Build属性为Embedded Resource,并添加如下代码映射Planet实体:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="LinqToNHibernateSample" namespace="LinqToNHibernateSample"> <class name="Planet"> <id name="Id"> <generator class="guid.comb"/> </id> <property name="Name"/> <property name="IsHabitable"/> <many-to-one name="Sun" class="Star" column="StarId"/> </class> </hibernate-mapping>
注意我们在many-to-one标签中使用column特性匹配Star.hbm.xml映射文件中bag标签定义的key。
12. 在项目中添加XML文件hibernate.cfg.xml。这个文件包含配置信息,代码如下所示:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2"> <session-factory name="Sample"> <property name="connection.provider"> NHibernate.Connection.DriverConnectionProvider </property> <property name="connection.driver_class"> NHibernate.Driver.SqlClientDriver </property> <property name="dialect"> NHibernate.Dialect.MsSql2008Dialect </property> <property name="connection.connection_string"> server=.;database=LinqToNHibernateSample;integrated security=true </property> <property name="proxyfactory.factory_class"> NHibernate.ByteCode.Castle.ProxyFactoryFactory,NHibernate.ByteCode.Castle </property> </session-factory> </hibernate-configuration>
13. 设置hibernate.cfg.xml文件的Copy to Output Directory属性为always。
14. 在Program类中的Main方法中,添加代码初始化Nhibernate Configuration对象,如下所示:
var configuration = new Configuration();
15. 嗲用configuration对象的Configure方法。这个方法会当前目录中搜索名为hibernate.cfg.xml的文件,如果找到,就从中读取所有的配置值,如下面的代码所示:
configuration.Configure();
16. 添加代码告诉configuration对象解析程序集,如下所示:
configuration.AddAssembly(typeof(Star).Assembly);
17. 添加代码创建或重新创建数据库架构,如下面的代码所示:
new SchemaExport(configuration).Execute(true, true, false);
18. 使用configuration对象创建一个session工厂,如下面的代码所示:
var factory = configuration.BuildSessionFactory();
19. 然后添加如下代码:
CreateData(factory); QueryData(factory); Console.Write("\r\n\nHit enter to exit:"); Console.ReadLine();
20. 下面我们首先实现CreateData方法,代码如下:
private static void CreateData(ISessionFactory factory) { var sun = new Star { Name = "Sun", Mass = 1, Class = StarTypes.G, Color = SurfaceColor.WhiteToYellow }; var planets = new List<Planet> { new Planet{Name = "Merkur", IsHabitable = false, Sun = sun}, new Planet{Name = "Venus", IsHabitable = false, Sun = sun}, // please consult the sample code for full list of planets }; sun.Planets = planets; var virginis61 = new Star { Name = "61 Virginis", Mass = 0.95, Class = StarTypes.G, Color = SurfaceColor.WhiteToYellow }; var planets2 = new List<Planet> { new Planet{Name = "Planet 1", IsHabitable = false,Sun = virginis61}, new Planet{Name = "Planet 2", IsHabitable = true,Sun = virginis61}, new Planet{Name = "Planet 3", IsHabitable = false,Sun = virginis61} }; virginis61.Planets = planets2; var stars = new List<Star> { sun, virginis61, new Star{Name = "10 Lacertra", Mass = 60,Class = StarTypes.O, Color = SurfaceColor.Blue}, new Star{Name = "Spica", Mass = 18,Class = StarTypes.B, Color = SurfaceColor.Blue}, // please consult the sample code for full list of stars }; using (var session = factory.OpenSession()) using (var tx = session.BeginTransaction()) { foreach (var star in stars) { session.Save(star); } tx.Commit(); } }
21. 接着我们实现QueryData方法,代码如下:
private static void QueryData(ISessionFactory factory) { using (var session = factory.OpenSession()) using (var tx = session.BeginTransaction()) { PrintListOfStars(session); PrintListOfBigBlueStars(session); PrintSumOfStarMassPerClass(session); PrintListOfHabitablePlanets(session); tx.Commit(); } }
注意这个方法是如何创建session,transaction以及在transaction内调用四个报表方法。下面让我们分别实现上面的四个方法。
22. 让我们首先实现第一个方法,它根据star的名称排序列表并打印,代码如下所示:
private static void PrintListOfStars(ISession session) { Console.WriteLine("\r\n\nList of stars ------------------\r\n"); var stars = session.Query<Star>().OrderBy(s => s.Name); foreach (var star in stars) { Console.WriteLine("{0} ({1}, {2})", star.Name, star.Class, star.Color); } }
注意上面的代码中,我们是如何使用Query扩展方法获得IQueryable<Star>,然后使用OrderBy方法排序的。此外,注意当我们开始遍历它时查询才会执行。
23. 下面实现第二个方法。我们使用多个属性筛选和排序,甚至是顺序颠倒过来,代码如下所示:
private static void PrintListOfBigBlueStars(ISession session) { Console.WriteLine("\r\n\nList of big blue stars -------\r\n"); var stars = session.Query<Star>() .Where(s => s.Color == SurfaceColor.Blue && s.Mass > 15) .OrderByDescending(s=>s.Mass) .ThenBy(s => s.Name); foreach (var star in stars) { Console.WriteLine("{0} ({1}, {2}, Mass={3})", star.Name, star.Class, star.Color, star.Mass); } }
24. 第三个方法,我们分组star列表,并使用一个聚合函数(Sum)计算存储在数据库中的star相对质量的总和。代码如下所示:
private static void PrintSumOfStarMassPerClass(ISession session) { Console.WriteLine("\r\n\nSum of masses per class -------\r\n"); var starMasses = session.Query<Star>() .GroupBy(s => s.Class) .Select(g => new { Class = g.Key, TotalMass = g.Sum(s => s.Mass) } ); foreach (var mass in starMasses) { Console.WriteLine("Class={0}, Total Mass={1}", mass.Class, mass.TotalMass); } }
25. 最后,我们实现的方法是打印按照Sun和行星的名称排序的适合人类居住的行星列表,代码如下所示:
private static void PrintListOfHabitablePlanets(ISession session) { Console.WriteLine("\r\n\nList of habitable planets------\r\n"); var planets = session.Query<Planet>() .Where(p => p.IsHabitable) .OrderBy(p => p.Sun.Name) .ThenBy(p => p.Name); foreach (var planet in planets) { Console.WriteLine("Star='{0}', Planet='{1}'", planet.Sun.Name, planet.Name); } }
26. 运行程序,结果如下图所示:
总结
首先我们介绍了使用LINQ to NHibernate提供程序查询的一些理论知识,然后通过一个例子
定义了不同的查询,包括筛选、排序、分组和聚合,并将结果集打印到屏幕上。