2016-07-16 46 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 ...)'。這就好像它只是用我的子選擇符替換表名,無論它感覺如何。 「 –

相關問題