2016-07-16 51 views
3

的考虑这个人为的域名:NHibernate的QueryOver排序每一个一对多的集合

namespace TryHibernate.Example { 

public class Computer 
{ 
    public int Id { get; set; } 
    public IList<Device> Devices { get; set; } 
} 

public class Device 
{ 
    public int Id { get; set; } 
    public Maker Maker { get; set; } 
} 

public class Maker 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 

} // namespace 

如果我只是查询所有计算机,他们的设备将被随机排序。我想让他们按制造商的名字订购。我不能用HasMany().OrderBy()这么做,因为据我所知OrderBy只能使用本地列(所以我可以用Device.Id来排序)。此外,它可以很好地控制每个查询的顺序,所以我正在寻找一个QueryOver解决方案。

我能得到最远的是这样的:

using (ISessionFactory sessionFactory = Fluently.Configure() 
    .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) 
    .Mappings(m => m.AutoMappings.Add(
     AutoMap.AssemblyOf<Computer>(new ExampleConfig()) 
      .Conventions.Add(DefaultLazy.Never()) 
      .Conventions.Add(DefaultCascade.All()))) 
    .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
    .BuildSessionFactory()) 
{ 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     Computer comp = new Computer(); 
     comp.Devices = new List<Device>(); 
     Device dev1 = new Device(); 
     comp.Devices.Add(dev1); 
     dev1.Maker = new Maker() { Name = "IBM"}; 
     Device dev2 = new Device(); 
     comp.Devices.Add(dev2); 
     dev2.Maker = new Maker() { Name = "Acer"}; 
     db.Persist(comp); 
     db.Flush(); 
    } 
    using (ISession db = sessionFactory.OpenSession()) 
    { // This is the part I'm having trouble with: 
     Device devAlias = null; 
     Maker makerAlias = null; 
     IList<Computer> comps = db.QueryOver<Computer>() 
      .JoinAlias(c => c.Devices,() => devAlias) 
      .JoinAlias(() => devAlias.Maker,() => makerAlias) 
      .OrderBy(() => makerAlias.Name).Asc 
      .List(); 
     Console.WriteLine(comps.Count); 
     foreach (Device dev in comps[0].Devices) 
     { 
      Console.WriteLine(dev.Maker.Name); 
     } 
    } 
} 

但是,当然,它不会做我想做的。它试图按制造商的名称对整个清单进行分类。它也成功了,正如我从SQL中看到的那样,实际上我得到了一个无用的笛卡尔计算机产品,其中包含由设备制造商分类的设备。

但它然后发出另一个SQL查询来获取设备,这次没有排序。我猜NHibernate不知道我的联接是为了取孩子。

问题是,我该如何控制第二个查询?例如,要按制造商的名称订购设备,或者要让每个Computer.Devices列表仅包含由IBM(如果有)制造的设备。我想我需要一个子查询,但我在哪里插入?

只是为了完整性,这里是我的配置:

class ExampleConfig : DefaultAutomappingConfiguration 
{ 
    public override bool ShouldMap(Type type) 
    { 
     return type.Namespace == "TryHibernate.Example"; 
    } 
} 

回答

1

经过与博基达的回答挣扎了一段时间后,我觉得它有点工作。我不得不为电脑,这样做手工映射:

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="all" default-lazy="false"> 
    <class name="TryHibernate.Example.Computer, TryHibernate" table="Computer"> 
    <id name="Id" type="System.Int32, mscorlib" column="Id" generator="identity" /> 
    <bag name="Devices"> 
     <key column="ComputerId" /> 
     <one-to-many class="TryHibernate.Example.Device, TryHibernate" /> 
     <loader query-ref="DeviceLoader" /> 
    </bag> 
    </class> 

    <sql-query name="DeviceLoader"> 
    <load-collection alias="Device" role="TryHibernate.Example.Computer.Devices" /> 
    SELECT Device.Id, Device.Maker_Id, Device.ComputerId, Maker.Name 
    FROM Device JOIN Maker ON (Maker.Id = Device.Maker_Id) 
    WHERE Device.ComputerId=? 
    ORDER BY Maker.Name 
    </sql-query> 
</hibernate-mapping> 

此进入名为Computer.hbm.xml嵌入的资源。然后代码工作就像这样:

using (ISessionFactory sessionFactory = Fluently.Configure() 
    .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) 
    .Mappings(m => m.AutoMappings.Add(
     AutoMap.AssemblyOf<Employee>(new ExampleConfig()) 
      .Conventions.Add(DefaultLazy.Never()) 
      .Conventions.Add(DefaultCascade.All()))) 
    .Mappings(m => m.HbmMappings.AddFromAssembly(Assembly.GetExecutingAssembly())) 
    .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
    .BuildSessionFactory()) 
{ 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     Computer comp = new Computer(); 
     comp.Devices = new List<Device>(); 
     Device dev1 = new Device(); 
     comp.Devices.Add(dev1); 
     dev1.Maker = new Maker() { Name = "IBM" }; 
     Device dev2 = new Device(); 
     comp.Devices.Add(dev2); 
     dev2.Maker = new Maker() { Name = "Acer" }; 
     db.Transaction.Begin(); 
     db.Persist(comp); 
     db.Transaction.Commit(); 
    } 
    using (ISession db = sessionFactory.OpenSession()) 
    { 
     IList<Computer> comps = db.QueryOver<Computer>().List(); 
     Console.WriteLine(comps.Count); 
     foreach (Device dev in comps[0].Devices) 
     { 
      Console.WriteLine(dev.Maker.Name); 
     } 
    } 
} 

但是,我不能说我很高兴这一点。首先,要订购一些东西似乎太多了,SQL支持开箱即用。并且它不能在查询级别进行定制,这是否会影响我的问题的目的。然后,这个标签让我感觉很不安。这意味着一个无序集合。当然,它似乎来维持秩序,但有没有任何保证?而愚蠢的NHibernate只是不允许在那里使用列表,因为,你看,它不会是一个“真实”的映射。你需要一个愚蠢的“索引”栏来获得真正的列表!

所有这些只是为了排序一些微不足道的东西。让我想到为什么像jOOQ这样的事情成为现实。 .Net需要类似的东西(任何人都可以?),绝望地。只需使用具有代码生成器的类型安全嵌入式SQL以及自动转换到/来自POCO。

+0

”看起来工作太多,只是为了订购一些东西,而SQL支持现成的“ - 完全同意。它应该更简单,但事实并非如此。 NHibernate经常有这种能力,既成熟又愚蠢;同时,更不用说文档质量......但NH中有LINQ支持。你见过吗? –

+0

@Bozhidar,对,我见过Linq,但我不确定它是否可以用在这种情况下,而且我不确定它是映射到SQL还是进行内存排序,这是毫无意义的因为我总是可以自己在取回的集合上做到这一点。 –

1

这不是真的NHibernate的支持。有很多功能可以结合使用,并可能接近它。这些解决方案往往很快就会中断,难以维护。

QueryOver:您可以告诉NHibernate加入孩子并同时将其填充到一个集合中。

db.QueryOver<Computer>() 
    .JoinAlias(c => c.Devices,() => devAlias) 
    .JoinAlias(() => devAlias.Maker,() => makerAlias) 
    .Fetch(x => x.Devices).Eager 
    .OrderBy(() => makerAlias.Name).Asc 
    .List(); 

由于笛卡尔产品问题,这对于一个集合有一定的作用。你的情况可能已经太复杂了。

映射:另一种解决方案是在映射文件中排序。当您将一些SQL添加到“公式”中时,您可能会按照制造商的名称来管理它。我不会这样做。它破坏了应用程序的性能,因为它需要所有这些连接和排序才能将这些东西从数据库中取出。

排序写作:你应该考虑把它按正确的顺序放到数据库中。将其映射为列表,而不是包。说实话:制造商订购的设备听起来更像是一种展示的东西,而不是数据管理的东西,因此应该在阅读时完成。例如在保持正确的顺序时也很难保持正确的顺序。制造商更改名称。不过,我希望为了完整而添加此选项,通常这是有道理的。

KISS:现在您可以获得最简单的解决方案。

-drum roll-

使用Linq在内存中对其进行排序。

foreach(var computer in computers) 
{ 
    foreach(var device in computer.Devices.OrderBy(x => x.Maker.Name)) 
    { 

    } 
} 

完成。

+0

'Fetch'方式对我来说不起作用:它仍然不会对结果列表中的计算机拥有的集合进行排序。 –

+0

@SergeyTachenov:可能它不工作。例如,当它是一个列表而不是袋子时,它甚至不允许改变顺序。正如我所说:很容易打破。 –

1

我建议给自己一个小时。在那个小时内,为这个特定查询编写一些实体框架处理。我已经将NHibernate转换为Entity框架,并且很可能您在所涉及的表格之间有一个随机的xml链接。在实体框架内,这些'导航'/'虚拟馆藏'在ORM中明确实施。

如果你不能在NHibernate中导航到你的对象,我建议你添加一小块实体框架到一个单独的类库来熟悉ORM,这将解释为什么你可能的多对多关系不起作用。如果你有一对多的关系,这应该像这样简单。

+0

这个例子的重点在于学习NHibernate,所以EF显然不会帮助我。你错过了另一点 - 它是流利的NHibernate,所以没有XML,没有愚蠢的属性,在代码中几乎没有手动映射。问题中提供的代码实际上完成了所有映射。 EF甚至可以这样做吗? –

+0

EF可以并且确实为你绘制东西。它甚至通过创建另一个导航属性为您的多对多创建一对多。我不得不将整个NHibernate安装程序迁移到实体框架,我仍然记得NH的隐含连接。 – Programmer

+0

我不喜欢EF依赖如何爬入域。让我想起了JPA。我不喜欢那样。但是,再次,我不喜欢ORM。 JPA并不是那么糟糕,所以也许我会给EF一个尝试。 –

2

您可以通过干涉Device es检索的SQL select语句来实现此目的。对我来说最优雅的解决方案是使用自定义加载器。但是这是
not supported through Fluent NHibernate。幸运的是,你可以在项目中的某处写单的hbm.xml并通过流畅,像这样添加:

Device.hbm.xml

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true"> 
    <class name="TryHibernate.Example.Device, TryHibernate" table="Device"> 
     <id name="Id" type="System.Int32, mscorlib" column="Id" generator="identity" /> 
     <property name="Name" /> 
     <many-to-one name="Maker" column="MakerId" /> 
     <many-to-one name="Computer" column="ComputerId" /> 
     <loader query-ref="DeviceLoader" /> 
    </class> 

    <sql-query name="DeviceLoader"> 
     <return alias="dev" class="TryHibernate.Example.Device, TryHibernate" lock-mode="read"> 
      <return-property name="Computer" column="ComputerId" /> 
      <return-property name="Maker" column="MakerId" /> 
     </return> 

     SELECT Device.Id AS {dev.Id}, Device.Name As {dev.Name}, Device.MakerId AS {dev.MakerId}, Device.ComputerId AS {dev.ComputerId} 
     FROM Device INNER JOIN Maker ON Maker.Id = Device.MakerId 
     WHERE Device.Id=? 
     ORDER BY Device.ComputerId, Maker.Name 
    </sql-query> 
</hibernate-mapping> 

然后同时建立会话工厂做:

 
    using (ISessionFactory sessionFactory = Fluently.Configure() 
     .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) 
     .Mappings(m => m.AutoMappings.Add(
      AutoMap.AssemblyOf(new ExampleConfig()) 
       .Conventions.Add(DefaultLazy.Never()) 
       .Conventions.Add(DefaultCascade.All()))) 
     .Mappings(m => m.HbmMappings.AddFromAssembly(Assembly.GetExecutingAssembly())) 
     .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) 
     .BuildSessionFactory()) 

另一个打击我的想法是,您可以为您的映射指定Subselect();像这样:

public class DeviceMap : ClassMap<Device> 
{ 
    public DeviceMap() 
    { 
     Table("Device"); 
     Subselect(@" 
    SELECT TOP 100 Device.Id , Device.Name, Device.MakerId , Device.ComputerId 
    FROM Device INNER JOIN Maker ON Maker.Id = Device.MakerId 
    ORDER BY Device.ComputerId, Maker.Name 
"); 

     Id(x => x.Id); 
     Map(x => x.Name); 

     References(x => x.Computer).Column("ComputerId"); 
     References(x => x.Maker).Column("MakerId"); 
    } 

正如你看到我用了TOP条款。这是必要的,因为子select语句变成一个子查询,正如你可能知道

ORDER BY子句在视图,内联函数,派生表,子查询和公用表表达式无效,除非TOP,抵消或FOR XML也被指定。


另一个虽然是实施IInterceptor接口和改变SQL合适。像这样:

public class SqlStatementInterceptor : EmptyInterceptor 
{ 
    public override SqlString OnPrepareStatement(SqlString sql) 
    { 
     var result = sql; 

     if (sql.IndexOfCaseInsensitive("FROM Device") != -1) 
     { 
      var sqlText = string.Format("WITH cteDev AS ({0}{1}{0}){0}SELECT cteDev.* FROM cteDev INNER JOIN Maker ON cteDev.MakerId1_0_ = Maker.Id ORDER BY Maker.Name", Environment.NewLine, sql.ToString()); 

      result = sql 
       .Insert(0, "WITH cteDev AS (") 
       .Append(")SELECT cteDev.* FROM cteDev INNER JOIN Maker ON cteDev.MakerId1_0_ = Maker.Id ORDER BY Maker.Name"); 
      ; 

     } 

     Trace.WriteLine(result.ToString()); 

     return result; 
    } 
} 

***使用这一个,你需要使用约定和图形NHibernate如何生成表和列的名称。

+0

哇,那是......意想不到的。距离赏金到期不到一天,我认为它会消失。我只尝试过你的第一个解决方案,因为其他人似乎更加复杂,没有任何交换。第一个工作并不完善,它不能让我按照每个查询来控制排序,但它仍然指向我,让我至少能以某种方式工作。详情请参阅我的回答。如果没有更好的答案,肯定会得到赏金! –

+0

尝试第二个选项 - “子选择(字符串)”。唯一的权衡是'顶级'条款似乎并不是什么新鲜事 - 我相信你可以根据你的情况计算出足够的记录数量......另外,这并不难实现;这对我来说是最快的方法。 –

+0

如果我做了一个'Subselect',当试图添加设备时,它总是试图做一个绝对荒谬的'INSERT INTO(SELECT ...)'。这就好像它只是用我的子选择符替换表名,无论它感觉如何。 “ –

相关问题