Проблема N+1 в mongoid

Я использую Mongoid для работы с MongoDB в Rails.

то, что я ищу, - это что-то вроде active record include. В настоящее время мне не удалось найти такой метод в mongoid orm.

кто-нибудь знает как решить эту проблему в mongoid или, возможно, в mongomapper, который известен как еще один хороший вариант.

8 ответов


обновление: прошло два года с тех пор, как я разместил этот ответ, и все изменилось. См.tybro0103 это для сведения.


Ответ

судя по документации обоих водителей, ни один из них не поддерживает то, что вы ищете. Вероятно, потому что это ничего не решает.

на :include функциональность ActiveRecord решает задачу N+1 для базы данных SQL. Рассказав ActiveRecord какие связанные таблицы включить, он может построить один SQL-запрос, используя JOIN заявления. В результате одного вызова базы данных, независимо от количества таблиц, которые вы хотите запросить.

MongoDB позволяет только запрос одной коллекции за раз. Он не поддерживает ничего, как JOIN. Поэтому, даже если вы можете сказать Mongoid, какие другие коллекции он должен включать, ему все равно придется выполнять отдельный запрос для каждого дополнительного коллекция.


Теперь, когда прошло некоторое время, Mongoid действительно добавил поддержку для этого. См. раздел "нетерпеливая загрузка" 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

Я хотел бы отметить:

  1. рельсы' :include не делает соединение
  2. SQL и Mongo оба нуждаются в нетерпеливой загрузке.
  3. проблема N+1 происходит в этом типе сценария (запрос, созданный внутри loop):

.

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

похоже, что ссылка ,которую @ amrnt опубликовал, была объединена в Mongoid.


хотя другие ответы верны, в текущих версиях Mongoid метод includes является лучшим способом достижения желаемых результатов. В предыдущих версиях, где 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 приводит к проблеме запроса n+2 из записи судьи. в моем случае предоставление серверу dev времени ответа:

завершено 200 OK в 816ms (просмотров: 785.2 ms)

ключ к решению этой проблемы-загрузить пользователей и фотографии в одном запросе вместо 1 на 1 на судью.

вы можете сделать это, используя Mongoids IdentityMap Mongoid 2 и Mongoid 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 для фотографий.

завершено 200 OK в 559ms (просмотров: 87.7 ms)

как эта работа? Что такое IdentityMap?

IdentityMap помогает отслеживать, какие объекты или записи уже были загружены. Поэтому, если вы получите первую запись Пользователя, IdentityMap сохранит ее. Затем, если вы попытаетесь снова получить того же пользователя, Mongoid запросит IdentityMap для пользователя, прежде чем он снова запросит базу данных. Это позволит сохранить 1 запрос в базе данных.

таким образом, загрузив всех пользователей и фотографии, которые мы знаем, мы будем хотеть для судей json в ручных запросах мы предварительно загружаем данные в IdentityMap все сразу. Затем, когда судья требует, чтобы это Пользователь и фото он проверяет IdentityMap и не нужно запрашивать базу данных.


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 lazy загружает все запросы, чтобы разрешить это поведение по умолчанию. The :include тег не нужен.



вам нужно обновить схему, чтобы избежать этого N+1 в MongoDB нет решения для выполнения некоторых соединений.


вставьте подробные записи / документы в главную запись / документ.


в моем случае у меня не было всей коллекции, но ее объект, который вызвал n+1 (пуля говорит об этом).

Так, а не писать ниже, что вызывает n+1

quote.providers.officialname

я писал

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

это не вызвало проблемы, но оставило меня думать, если я повторюсь или проверка n+1 не нужна для mongoid.