如何最好地在 NHibernate 中检索和更新这些对象?

Posted

技术标签:

【中文标题】如何最好地在 NHibernate 中检索和更新这些对象?【英文标题】:How best to retrieve and update these objects in NHibernate? 【发布时间】:2009-11-12 11:56:22 【问题描述】:

我之前曾向a question 询问过关于用户、项目和用户评级的情况建模。在我的示例中,用户评级与一个用户和一个项目相关联。 A Nathan Fisher 提供了很好的答案,我在下面提供了他建议的模型。

但我现在有一个关于检索这些对象的问题。

模型通过保存对实体的引用来链接实体。我的问题是,如何才能最好地检索要更新的特定 UserRating? 在这种情况下我将拥有用户 ID(来自 asp.net 身份验证会话)、和 itemID(来自 URL)。此外,每个用户或项目可能有 1000 多个评分。

回到老式学校,这就像一个更新查询'其中 x = userID 和 y=itemID 一样简单。简单的。然而,在 NHibernate 中使用适当的对象模型来完成此任务的最佳方法还不是很清楚。

A) 我知道我可以创建一个存储库方法 GetRatingByUserAndItem,并将用户和项目对象同时传递给它,它会对其执行 HQL/标准查询以检索评级 但是要做到这一点,我假设我首先需要从 ORM 中检索用户和项目,然后再将它们传递回查询中的 ORM。然后我会获取 UserRating 对象,更新它,然后让 ORM 持久化更改。与旧学校的方法相比,这对我来说似乎效率低得离谱。

B) 也许我可以新建 UserRating 对象,然后创建或更新类型调用 ORM(不确定确切的语法)。这样会更好,但大概我仍然需要先检索用户和项目,这仍然非常低效。

C) 也许我应该从 ORM 中检索用户(或项目),并从其 UserRatings 集合中找到正确的 UserRating。 但是,如果我这样做了那,我如何确保我没有检索到与该用户(或项目)相关的所有 UserRatings,而只是与特定项目和特定用户相关的一个?

D) 我突然想到,我可以从模型中的 UserRating 中删除对 User 和 Item 的完整引用,而使用原始引用(UserID 和 ItemID)。 这将允许我做一些像老式方法一样简单的事情。非常诱人,但这对我来说似乎不太合适——不是非常面向对象(这肯定是我们首先使用 ORM 的主要原因!)

那么,任何人都可以提供一些明智的建议吗?我是否在正确的轨道上使用上述任何选项?或者有没有更好的方法我没有考虑过?

提前感谢您的帮助! :)

更新:

我刚刚为此发布了赏金,并且更好地理解了这一点,我还想知道,使用类似的方法,如何最好地执行以下查询

检索用户未评分的所有项目。 检索用户评分最低的项目和项目评分。

模型如下:

public class User 

    public virtual int UserId  get; set; 
    public virtual string UserName  get; set; 
    public virtual IList<UserRating> Ratings  get; set; 


public class Item 

    public virtual int ItemId  get; set; 
    public virtual string ItemName  get; set; 
    public virtual IList<UserRating> Ratings  get; set; 

public class UserRating 

    public virtual User User  get; set; 
    public virtual Item Item  get; set; 
    public virtual Int32 Rating  get; set; 


<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Test" namespace="Test" >
    <class name="User">
        <id name="UserId" >
                <generator class="native" />
        </id>
        <property name="UserName" />
        <bag name="Ratings" generic="true" inverse="true" table="UserRating">
                <key column="UserId" />
                <one-to-many class="UserRating"/>
        </bag>
    </class>
    <class name="Item" >
        <id name="ItemId" >
                <generator class="native" />
        </id>
        <property name="ItemName" />
        <bag name="Ratings" generic="true" inverse="true" table="UserRating">
                <key column="ItemId" />
                <one-to-many class="UserRating"/>
        </bag>
    </class>
    <class name="UserRating" >
        <composite-id>
                <key-many-to-one class="User" column="UserId" name="User" />
                <key-many-to-one class="Item" column="ItemId" name="Item" />
        </composite-id>
        <property name="Rating" />
    </class>
</hibernate-mapping>

【问题讨论】:

【参考方案1】:

通常,当您使用 ORM 时,您希望实现面向对象的业务逻辑(例如:更改数据)。这需要从数据库中加载对象。 NH 允许您加载它们一次,并在不参考任何数据库相关内容的情况下更改它,而只是更改属性值。

这就是说,它并不总是那么容易。有时出于性能原因需要其他方式来更新数据。

您可以使用 HQL updates 甚至 SQL 更新。

另一种更经典的方法是只加载UserRatings。这需要使它成为一个独立的实体(它需要一个 id,无论如何都要避免复合 id,用多对一的引用来复制它)。然后按用户和项目过滤UserRatings,将要更改的项目加载到数据库中,并使用面向对象编程进行更改。

它总是在性能和​​面向对象编程之间进行权衡。你应该尽量做到面向对象,只有在需要的时候才做优化。可维护性很重要。

我会避免将外键移动到域模型。

【讨论】:

谢谢。那么我将如何加载 UserRatings 并按用户和项目过滤呢?我不熟悉过滤器,您是指 NHibernate 文件管理器,还是只是使用例如过滤结果集林克? 只是为了添加到我上面的评论中 - 我不想从数据库中获取多条记录,然后以编程方式选择我想要的一条。 但我想,在那种情况下,我首先需要加载用户并加载项目,对吧? 不,您可以直接查询 UserRatings:from UserRatings where User.id = :userId and Item.id = :itemId 或类似的东西。延迟加载将阻止加载 UserRating 时加载用户和项目。此示例假定 User 和 Item 在 UserRating 类中是多对一的引用。【参考方案2】:

我会选择选项 C。您对性能的担忧表明您可能过早地进行了优化。我认为您最好有一个 GetUser(int userId) 方法,然后在其 Ratings 集合中查找适当的项目并更新它。

然而,这确实带来了 ORM 遭受的一个常见问题,称为 N+1 SELECT 问题。查看每个 UserRating 以找到合适的可能会导致每个 UserRating 有一个 SELECT 语句。有几种方法可以解决这个问题。一种是更改映射文件以禁用 Ratings 集合的延迟加载,或者使用“加入”获取加载它 - 请参阅 NHibernate 文档的this section。

【讨论】:

感谢 Serilla。我不喜欢过早地优化——但我认为将数据库查询的数量保持在最低限度非常重要。问题是,可能有 1000 多个用户评分与用户相关联 - 因此,每次我遍历集合以找到正确的评分时都不能进行查询。不过,连接获取听起来很有趣 - 我会检查一下。 +1 表示“过早优化”。我实际上试图在我的回答中表达同样的意思,我只是不确定如果每个用户有数百甚至数千个用户评分,这是否不是一个严重的性能问题。 Stefan,你没有看到有 100 次查询来获取一个我可以在没有 ORM 的情况下只用 1 更新的对象的问题吗? @Sosh:为什么你会收到 100 个查询?在更新对象之前,您只需要一个选择来将对象放入内存。与面向对象的业务逻辑相比,这有点开销。【参考方案3】:

使用 HQL,您的查询将如下所示

Select From UserRating
Where ItemId=: @ItemId
and UserId=: @UserId

这将为您提供您之后的 UserRating 对象,然后您可以根据需要更新并保存它

另一种选择是

Session.CreateCriteria(typeof(ClassLibrary1.UserRating))
                    .Add(Expression.Sql(String.Format("ItemId=0",UserId)))
                    .Add(Expression.Sql(String.Format("UserId=0",ItemId)))
                    .List<ClassLibrary1.UserRating>();

这是我可以让它工作的最简单的方法。我对嵌入的字符串不满意,但它可以工作。

【讨论】:

如果 Item 和 User 是多对一引用,则不需要Expression.Sql,也不需要string.Format。只需Expression.IdEq,非常直接。 我仍处于 ICriteria 界面的早期学习阶段。我玩过Expression.IdEq,但它一直在创建无效的 SQL。继续在 where 子句中将 UserId 添加为 @y_0。我最终得到了我知道有效的上述结果。我同意不是很优雅。【参考方案4】:
public void UpdateRating( int userId, int itemId, int newRating, ISession session )

  using( var tx = session.BeginTransaction())
  
    var ratingCriteria = session.CreateCriteria<UserRating>()
      .CreateAlias( "Item" "item" )
      .CreateAlias( "User" "user" )
      .Add( Restrictions.Eq( "item.ItemId", itemId ) )
      .Add( Restrictions.Eq( "user.UserId", userId ) );
    var userRating = ratingCriteria.UniqueResult<UserRating>();
    userRating.Rating = newRating;
    tx.Commit();
  

您需要对此进行测试,因为自从我使用标准 api 以来已经有一段时间了,但本质上它所做的是为两个关联路径创建别名,然后使用这些别名,向用户和项目添加限制,以便您只获取您感兴趣的 UserRating。

一切都在事务中完成,依靠 NHibernate 的脏状态跟踪,当事务提交时,更改将刷新到数据库。

根据您使用的 NHibernate 版本,您还可以使用 NHLinq 或已集成且现在可通过 session.QueryOver&lt;T&gt; 访问的 NHLambda 内容进行查询。

要检索用户未评分的所有项目的列表,您将需要使用子查询来识别用户已评分的所有项目,然后将 not in 子句应用于所有项目(基本上得到我所有不在用户评分的项目中的项目。

var ratedItemsCriteria = DetachedCriteria.For<UserRating>()
    .CreateAlias( "Item" "item" )
    .SetProjection( Projections.Property( "item.ItemId" ) )
      .CreateCriteria( "User" )
        .Add( Restrictions.Eq( "UserId", userId ) );
var unratedItemsCriteria = session.CreateCriteria<Item>()
  .Add( Subqueries.PropertyNotIn( "ItemId", ratedItemsCriteria ) );
var unratedItems - unratedItemsCriteria.List<Item>();

总的来说,我认为您的大部分问题都可以通过明智地应用 google、nhforge 和 nhibernate 用户邮件列表来解决。

【讨论】:

【参考方案5】:
    查询数据库(使用 HQL、Criteria、SQL 查询等)以获取您要修改的 UserRating 随意更改 UserRating 提交您的更改

在伪代码中,这看起来像这样:

using (ITransaction transaction = session.BeginTransaction())

    UserRating userRating = userRatingRepository.GetUserRating(userId, itemId);
    userRating.Rating = 5;
    transaction.Commit();

这将涉及两个查询(与一个查询“老派”解决方案相反)。第一个查询(发生在 GetUserRating 调用中)将运行 SQL“SELECT”以从数据库中获取 UserRating。第二个查询(发生在 transaction.Commit 上)将更新数据库中的 UserRating。

GetUserRating(使用 Criteria)看起来像这样:

public IList<UserRating> GetUserRating(int userId, int itemId)

    session.CreateCriteria(typeof (UserRating))
        .Add(Expression.Eq("UserId", userId))
        .Add(Expression.Eq("ItemId", itemId))
        .List<UserRating>();

【讨论】:

【参考方案6】:

我看到这个问题没有被标记为已回答,所以我会试一试。在我看来,您必须大量了解对象的使用方式。在我看来,您将 UserRating 更多地与项目相关联,而不是与用户相关,仅仅是因为您将在 UI 中的项目旁边显示它。始终为用户显示它并不重要。

这就是为什么我要删除用户的评分列表:

public class User 

    public virtual int UserId  get; set; 
    public virtual string UserName  get; set; 

如果您想要用户的评分,您可以通过存储库获得。

我会保持 Item 类不变,因为您总是希望查看某个项目的评分:

public class Item 

    public virtual int ItemId  get; set; 
    public virtual string ItemName  get; set; 
    public virtual IList<UserRating> Ratings  get; set; 

UserRating 类可以与 Item 和 User 类完全断开。只需将 ID 保留在其中,以便在需要时从存储库中检索项目或用户:

public class UserRating 

    public virtual int UserId  get; set; 
    public virtual int ItemId  get; set; 
    public virtual Int32 Rating  get; set; 

【讨论】:

以上是关于如何最好地在 NHibernate 中检索和更新这些对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何让 NHibernate ISession 缓存主键未检索到的实体

如何最好地在 ASP.NET 中填充 HTML 表?

如何使用 nhibernate 更新主键

nhibernate - sproutcore:如何只检索参考 ID 而不加载参考/关系?

插入时 NHibernate 组件非空属性

如何使用 NHibernate 检索具有列表条件的元素