如何最好地在 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<T>
访问的 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 缓存主键未检索到的实体