对 HABTM 关系(Mongoid、RoR)的 MongoDB 条件聚合查询?

MongoDB conditional aggregate query on a HABTM relationship (Mongoid, RoR)?

提问人:ljlozano 提问时间:2/18/2016 最后编辑:ljlozano 更新时间:5/17/2016 访问量:946

问:

Rails 4.2.5,Mongoid 5.1.0

我有三个模型 - 、 和 。MailboxCommunicationMessage

mailbox.rb

class Mailbox
    include Mongoid::Document
    belongs_to :user
    has_many :communications
end

communication.rb

class Communication
    include Mongoid::Document
    include Mongoid::Timestamps
    include AASM

    belongs_to :mailbox
    has_and_belongs_to_many :messages, autosave: true

    field :read_at,     type: DateTime
    field :box,         type: String
    field :touched_at,  type: DateTime
    field :import_thread_id, type: Integer
    scope :inbox, -> { where(:box => 'inbox') }
end

message.rb

class Message
    include Mongoid::Document
    include Mongoid::Timestamps

    attr_accessor :communication_id

    has_and_belongs_to_many :communications, autosave: true
    belongs_to :from_user, class_name: 'User'
    belongs_to :to_user, class_name: 'User'

    field :subject, type: String
    field :body,    type: String
    field :sent_at, type: DateTime
end

我正在使用 身份验证 gem ,它允许访问帮助程序,该帮助程序指向当前登录的用户。devisecurrent_user

我为满足以下条件的控制器构建了一个查询: 获取 's,其 's 由字段过滤,其中 . 它是这样构造的(并且正在工作):current_usermailboxcommunicationboxbox == 'inbox'

current_user.mailbox.communications.where(:box => 'inbox')

当我尝试建立这个查询时,我的问题就出现了。我希望链接查询,以便我只获取不是来自 .我知道 .last 方法,它返回最新的记录。我提出了以下查询,但无法理解需要调整什么才能使其正常工作:messageslastcurrent_user

current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})

此查询将生成以下结果:undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>

我目前能够通过执行以下操作来实现此目的,我知道这是非常低效的,并希望立即更改:

mb = current_user.mailbox.communications.inbox

comms = mb.reject {|c| c.messages.last.from_user == current_user}

我希望将此逻辑从 ruby 移动到实际的数据库查询。提前感谢您帮助我完成此工作的人,如果此处的信息有帮助,请告诉我。

Ruby-on-Rails Mongoid MongoDB 查询 聚合框架

评论

0赞 PJSCopeland 2/18/2016
我不认为 ActiveRecord 可以为您做到这一点 - 基于聚合(最后一个)的条件可能太复杂了。您可能需要求助于原始 SQL。
0赞 Nick Roz 2/20/2016
有错误吗?你写。(条件是评论)但在(条件是沟通where(:messages.last.from_user => {'$ne' => current_user})current_user.mailbox.communications.reject{ |c| c.last.from_user == current_user })
0赞 Nick Roz 2/20/2016
@PJSCopeland,mongo 不是 SQL 数据库
1赞 Nick Roz 2/20/2016
@ljlozano,也许你正在寻找 stackoverflow.com/questions/5550253/......docs.mongodb.org/v3.0/reference/operator/aggregation/last(也是聚合)。所以你的问题是如何在mongo db中使用聚合条件
0赞 ljlozano 2/20/2016
@NickRoz 对不起,那是错别字。我已经更新了我的问题。我现在也要看看这些链接。

答:

0赞 TreyE 5/17/2016 #1

好吧,所以这里发生的事情有点混乱,并且与Mongoid在进行关联时实际上能够变得多么聪明有关。

具体来说,在两个关联之间“交叉”时如何构造查询。

对于您的第一个查询:

current_user.mailbox.communications.where(:box => 'inbox')

这对 mongoid 来说很酷,因为它实际上只是脱糖成真正的 2 db 调用:

  1. 获取用户的当前邮箱
  2. Mongoid 直接针对通信集合构建了一个条件,其中的语句说:使用项目 1 中的邮箱 ID,并筛选为 box = inbox。

现在,当我们进入您的下一个查询时,

current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})

是Mongoid开始感到困惑的时候。

这是主要问题:当你使用“where”时,你正在查询你所在的集合。你不会交叉协会

where(:messages.last.from_user => {'$ne' => current_user}) 实际上所做的不是检查消息关联。Mongoid 实际上正在做的是在通信文档中搜索具有类似于 commation['messages']['last']['from_user'] 的 JSON 路径的属性。

现在你知道为什么了,你可以得到你想要的东西,但它需要比同等的ActiveRecord工作更多的汗水。

以下是您可以获得所需内容的更多方法:

user_id = current_user.id
communication_ids = current_user.mailbox.communications.where(:box => 'inbox').pluck(:_id)
# We're going to need to work around the fact there is no 'group by' in
# Mongoid, so there's really no way to get the 'last' entry in a set
messages_for_communications = Messages.where(:communications_ids => {"$in" => communications_ids}).pluck(
  [:_id, :communications_ids, :from_user_id, :sent_at]
)
# Now that we've got a hash, we need to expand it per-communication,
# And we will throw out communications that don't involve the user
messages_with_communication_ids = messages_for_communications.flat_map do |mesg|
  message_set = []
  mesg["communications_ids"].each do |c_id|
    if communication_ids.include?(c_id)
      message_set << ({:id => mesg["_id"],
       :communication_id => c_id,
       :from_user => mesg["from_user_id"],
       :sent_at => mesg["sent_at"]})
    end
  message_set
end
# Group by communication_id
grouped_messages = messages_with_communication_ids.group_by { |msg| mesg[:communication_id] }
communications_and_message_ids = {}
grouped_messages.each_pair do |k,v|
  sorted_messages = v.sort_by { |msg| msg[:sent_at] }
  if sorted_messages.last[:from_user] != user_id
    communications_and_message_ids[k] = sorted_messages.last[:id]
  end
end
# This is now a hash of {:communication_id => :last_message_id}
communications_and_message_ids

我不确定我的代码是 100%的(您可能需要检查文档中的字段名称以确保我正在搜索正确的字段名称),但我认为您得到了一般模式。