mongoid 中的 N+1 问题

发布于 2024-09-27 01:33:49 字数 176 浏览 3 评论 0原文

我正在使用 Mongoid 在 Rails 中使用 MongoDB。

我正在寻找类似活动记录 include 的内容。目前我在 mongoid orm 中找不到这样的方法。

任何人都知道如何在 mongoid 或 mongmapper 中解决这个问题,这被认为是另一个不错的选择。

I'm using Mongoid to work with MongoDB in Rails.

What I'm looking for is something like active record include. Currently I failed to find such method in mongoid orm.

Anybody know how to solve this problem in mongoid or perhaps in mongomapper, which is known as another good alternative.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

美人骨 2024-10-04 01:33:49

现在一段时间过去了,Mongoid 确实增加了对此的支持。请参阅此处的“预加载”部分:
http://docs.mongodb.org/ecosystem/tutorial /ruby-mongoid-tutorial/#eager-loading

Band.includes(:albums).each do |band|
  p band.albums.first.name # Does not hit the database again.
end

我想指出:

  1. Rails 的 :include 不执行连接
  2. SQL 和 Mongo 都需要急切加载。
  3. N+1 问题发生在这种类型的场景中(循环内生成的查询):

<% @posts.each do |post| %>
  <% post.comments.each do |comment| %>
    <%= comment.title %>
  <% end %>
<% end %>

看起来 @amrnt 发布的链接已合并到 Mongoid 中。

Now that some time has passed, Mongoid has indeed added support for this. See the "Eager Loading" section here:
http://docs.mongodb.org/ecosystem/tutorial/ruby-mongoid-tutorial/#eager-loading

Band.includes(:albums).each do |band|
  p band.albums.first.name # Does not hit the database again.
end

I'd like to point out:

  1. Rails' :include does not do a join
  2. SQL and Mongo both need eager loading.
  3. The N+1 problem happens in this type of scenario (query generated inside of loop):

.

<% @posts.each do |post| %>
  <% post.comments.each do |comment| %>
    <%= comment.title %>
  <% end %>
<% end %>

Looks like the link that @amrnt posted was merged into Mongoid.

绻影浮沉 2024-10-04 01:33:49

更新:自从我发布这个答案以来已经两年了,事情已经发生了变化。有关详细信息,请参阅 tybro0103 的回答


旧答案

根据两个驱动程序的文档,它们都不支持您正在寻找的内容。可能是因为它不能解决任何问题

ActiveRecord 的 :include 功能解决了 SQL 数据库 的 N+1 问题。通过告诉 ActiveRecord 要包含哪些相关表,它可以使用 JOIN 语句构建单个 SQL 查询。无论您要查询的表数量如何,这都将导致一次数据库调用。

MongoDB 只允许您一次查询一个集合。它不支持诸如JOIN之类的东西。因此,即使您可以告诉 Mongoid 它必须包含哪些其他集合,它仍然必须为每个附加集合执行单独的查询。

Update: it's been two years since I posted this answer and things have changed. See tybro0103's answer for details.


Old Answer

Based on the documentation of both drivers, neither of them supports what you're looking for. Probably because it wouldn't solve anything.

The :include functionality of ActiveRecord solves the N+1 problem for SQL databases. By telling ActiveRecord which related tables to include, it can build a single SQL query, by using JOIN statements. This will result in a single database call, regardless of the amount of tables you want to query.

MongoDB only allows you to query a single collection at a time. It doesn't support anything like a JOIN. So even if you could tell Mongoid which other collections it has to include, it would still have to perform a separate query for each additional collection.

深者入戏 2024-10-04 01:33:49

尽管其他答案是正确的,但在当前版本的 Mongoid 中,includes 方法是实现所需结果的最佳方法。在以前的版本中,包含不可用,我找到了一种摆脱 n+1 问题的方法,并认为值得一提。

就我而言,这是一个 n+2 问题。

class Judge
  include Mongoid::Document

  belongs_to :user
  belongs_to :photo

  def as_json(options={})
    {
      id: _id,
      photo: photo,
      user: user
    }
  end
end

class User
  include Mongoid::Document

  has_one :judge
end

class Photo
  include Mongoid::Document

  has_one :judge
end

控制器操作:

def index
  @judges = Judge.where(:user_id.exists => true)
  respond_with @judges
end

此 as_json 响应会导致 Judge 记录出现 n+2 查询问题。在我的例子中,给开发服务器的响应时间为:

在 816 毫秒内完成 200 OK(视图:785.2 毫秒)

解决此问题的关键是在单个查询中加载用户和照片,而不是每位法官 1 比 1。

您可以利用 Mongoid IdentityMap Mongoid 2Mongoid 3 支持此功能。

首先在 mongoid.yml 配置文件中打开身份映射:

development:
  host: localhost
  database: awesome_app
  identity_map_enabled: true

现在更改控制器操作以手动加载用户和照片。注意:Mongoid::Relation 记录将延迟评估查询,因此您必须调用 to_a 来实际查询记录并将它们存储在 IdentityMap 中。

def index
  @judges ||= Awards::Api::Judge.where(:user_id.exists => true)
  @users = User.where(:_id.in => @judges.map(&:user_id)).to_a
  @photos = Awards::Api::Judges::Photo.where(:_id.in => @judges.map(&:photo_id)).to_a
  respond_with @judges
end

这导致总共只有 3 个查询。 1 名评委,1 名用户,1 名照片。

在 559 毫秒内完成 200 OK(观看次数:87.7 毫秒)

这是如何工作的?什么是 IdentityMap?

IdentityMap 有助于跟踪已加载的对象或记录。因此,如果您获取第一个用户记录,IdentityMap 将存储它。然后,如果您尝试再次获取同一用户,Mongoid 会在再次查询数据库之前查询该用户的 IdentityMap。这将在数​​据库上节省 1 个查询。

因此,通过加载所有用户和照片,我们知道在手动查询中需要法官 json,我们将数据一次性预加载到 IdentityMap 中。然后,当法官需要用户和照片时,它会检查 IdentityMap,而不需要查询数据库。

Although the other answers are correct, in current versions of Mongoid the includes method is the best way to achieve the desired results. In previous versions where includes was not available I have found a way to get rid of the n+1 issue and thought it was worth mentioning.

In my case it was an n+2 issue.

class Judge
  include Mongoid::Document

  belongs_to :user
  belongs_to :photo

  def as_json(options={})
    {
      id: _id,
      photo: photo,
      user: user
    }
  end
end

class User
  include Mongoid::Document

  has_one :judge
end

class Photo
  include Mongoid::Document

  has_one :judge
end

controller action:

def index
  @judges = Judge.where(:user_id.exists => true)
  respond_with @judges
end

This as_json response results in an n+2 query issue from the Judge record. in my case giving the dev server a response time of:

Completed 200 OK in 816ms (Views: 785.2ms)

The key to solving this issue is to load the Users and the Photos in a single query instead of 1 by 1 per Judge.

You can do this utilizing Mongoids IdentityMap Mongoid 2 and Mongoid 3 support this feature.

First turn on the identity map in the mongoid.yml configuration file:

development:
  host: localhost
  database: awesome_app
  identity_map_enabled: true

Now change the controller action to manually load the users and photos. Note: The Mongoid::Relation record will lazily evaluate the query so you must call to_a to actually query the records and have them stored in the IdentityMap.

def index
  @judges ||= Awards::Api::Judge.where(:user_id.exists => true)
  @users = User.where(:_id.in => @judges.map(&:user_id)).to_a
  @photos = Awards::Api::Judges::Photo.where(:_id.in => @judges.map(&:photo_id)).to_a
  respond_with @judges
end

This results in only 3 queries total. 1 for the Judges, 1 for the Users and 1 for the Photos.

Completed 200 OK in 559ms (Views: 87.7ms)

How does this work? What's an IdentityMap?

An IdentityMap helps to keep track of what objects or records have already been loaded. So if you fetch the first User record the IdentityMap will store it. Then if you attempt to fetch the same User again Mongoid queries the IdentityMap for the User before it queries the Database again. This will save 1 query on the database.

So by loading all of the Users and Photos we know we are going to want for the Judges json in manual queries we pre-load the data into the IdentityMap all at once. Then when the Judge requires it's User and Photo it checks the IdentityMap and does not need to query the database.

热鲨 2024-10-04 01:33:49

ActiveRecord :include 通常不会执行完全连接来填充 Ruby 对象。它执行两次调用。首先获取父对象(例如帖子),然后第二次调用以拉取相关对象(属于帖子的评论)。

Mongoid 对于引用关联的工作方式本质上是相同的。

def Post
    references_many :comments
end

def Comment
    referenced_in :post
end

在控制器中,您获得帖子:

@post = Post.find(params[:id])

在您的视图中,您迭代评论:

<%- @post.comments.each do |comment| -%>
    VIEW CODE
<%- end -%>

Mongoid 将在集合中找到该帖子。当您点击评论迭代器时,它会执行单个查询来获取评论。 Mongoid 将查询包装在游标中,因此它是一个真正的迭代器,并且不会使内存过载。

Mongoid 延迟加载所有查询以默认允许此行为。 :include 标签是不必要的。

ActiveRecord :include typically doesn't do a full join to populate Ruby objects. It does two calls. First to get the parent object (say a Post) then a second call to pull the related objects (comments that belong to the Post).

Mongoid works essentially the same way for referenced associations.

def Post
    references_many :comments
end

def Comment
    referenced_in :post
end

In the controller you get the post:

@post = Post.find(params[:id])

In your view you iterate over the comments:

<%- @post.comments.each do |comment| -%>
    VIEW CODE
<%- end -%>

Mongoid will find the post in the collection. When you hit the comments iterator it does a single query to get the comments. Mongoid wraps the query in a cursor so it is a true iterator and doesn't overload the memory.

Mongoid lazy loads all queries to allow this behavior by default. The :include tag is unnecessary.

∝单色的世界 2024-10-04 01:33:49

你需要更新你的模式来避免这种 N+1,MongoDB 中没有解决方案来进行一些联合操作。

You need update your schema to avoid this N+1 there are no solution in MongoDB to do some jointure.

杯别 2024-10-04 01:33:49

将详细记录/文档嵌入主记录/文档中。

Embed the detail records/documents in the master record/document.

仅冇旳回忆 2024-10-04 01:33:49

就我而言,我没有整个集合,但有一个导致 n+1 的对象(项目符号是这么说的)。

没有在下面写导致 n+1 的内容,而是

quote.providers.officialname

因此,我

Quote.includes(:provider).find(quote._id).provider.officialname

写了这并没有造成问题,但让我思考是否重复自己或检查 n+1 对于 mongoid 来说是不必要的。

In my case I didn't have the whole collection but an object of it that caused n+1 (bullet says that).

So rather than writing below which causes n+1

quote.providers.officialname

I wrote

Quote.includes(:provider).find(quote._id).provider.officialname

That didn't cause a problem but left me thinking if I repeated myself or checking n+1 is unnecessary for mongoid.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文