NHibernate QueryOver 对每个 one-to-many 集合进行排序

NHibernate QueryOver to sort each of the one-to-many collections

考虑这个人为的域:

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 对它进行排序)。此外,在 per-query 的基础上控制排序会很好,所以我正在寻找 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 不知道我的连接是为了获取 children.

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

为了完整起见,这是我的配置:

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

NHibernate 并不真正支持这一点。您可以组合许多功能,并且可能会接近它。这些解决方案往往会很快崩溃并且难以维护。

QueryOver: 你可以告诉NHibernate加入children同时填充到一个collection中。

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

由于笛卡尔积问题,这在一个集合中效果较好。你的情况可能已经太复杂了。

映射:另一种解决方案是在映射文件中排序。当您将一些 SQL 添加到 "formula" 中时,您可能设法按制造商的名称进行订购。我不会那样做。它破坏了应用程序的性能,因为它需要所有这些连接和排序只是为了将这些东西从数据库中取出。

写上排序: 你应该考虑把它按正确的顺序放到数据库中。将其映射为 List,而不是 Bag。老实说:制造商订购的设备听起来更像是一个显示的东西而不是数据管理的东西,因此应该在阅读时完成。也很难保持正确的顺序,例如。厂商改名。但是,为了完整性,我想添加此选项,通常这是有道理的。

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

-击鼓-

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

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

  }
}

完成。

我建议给自己一个小时。在那一小时,为这个特定的查询写一些 entity framework 处理。我已将 NHibernate 转换为 Entity framework,很可能您在相关表之间有一个随机 xml 链接。在 entity framework 中,这些 'navigation'/'virtual collections' 在 ORM 中明确实现。

如果您无法在 NHibernate 中导航到您的对象,我建议您将 Entity Framework 的一小部分添加到单独的 Class 库中以熟悉 ORM很多关系都不行。如果您有一对多关系,这应该像 this.that.

一样简单

您可以通过干扰 SQL select 语句来实现 Devicees 检索。对我来说最优雅的解决方案是使用自定义加载器。但这是
not supported through Fluent NHibernate。幸运的是,您可以在项目中的某个地方编写单个 hbm.xml,然后像这样通过 fluent 添加它:

设备。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 子句。这是必要的,因为 sub-select 语句变成了一个子查询,你可能知道

The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP, OFFSET or FOR XML is also specified.


还有一个方法是实现一个 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 如何生成表和列的名称。

在为 Bozhidar 的回答苦苦思索了一段时间之后,我觉得它有点奏效了。我不得不为 Computer 手动映射,如下所示:

<?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 支持开箱即用。而且它不能在查询级别进行自定义,这违背了我提出问题的目的。然后,bag这个标签让我感到很不安。这意味着 无序 集合。当然,似乎 保留顺序,但是有任何保证吗?愚蠢的 NHibernate 不允许在那里使用 list 因为,你看,它不会是一个“真正的”映射。您需要一个愚蠢的“索引”列才能获得真正的列表!

所有这些只是为了解决一些琐碎的事情。让我思考为什么像 jOOQ 这样的东西会出现。 .Net 迫切需要类似的东西(nOOQ 有人吗?)。只需将类型安全嵌入 SQL 与代码生成器和自动转换 to/from POCOs 一起使用。