Cjoerg
Cjoerg

Reputation: 1325

How to extract Mongoid documents based on a field value in the first or last embedded document?

I wish to find Order documents based on a field in the last embedded Notificationdocument.

In the example below I wish to find all pending orders that has one or more embedded notifications, and where the last notification has a datetime that is between 5 and 10 days old.

My suggestion here dosen't seem to do the trick...:

Order.where(status: 'pending').gte('notifications.last.datetime' => 5.days.ago).lte('notifications.last.datetime' => 10.days.ago)

Here are the two models:

class Order
  include Mongoid::Document
  field :datetime, type: DateTime
  field :status, type: String, default: 'pending'
  embeds_many :notifications,  :inverse_of => :order
end

class Notification
  include Mongoid::Document
  field :datetime, type: DateTime
  embedded_in :order, :inverse_of => :notifications
end

Upvotes: 2

Views: 348

Answers (2)

Arthur Corenzan
Arthur Corenzan

Reputation: 911

I know it comes a little late, but hey, better later than never. :P

You can use JavaScript in where:

Order.where("this.notifications[this.notifications.length - 1].datetime > new Date('#{5.days.ago}')")

Just found out that and was a huge relief having not to change my models. Hope that helps!

Upvotes: 0

Akihiro HARAI
Akihiro HARAI

Reputation: 634

The main issue of the question seems to be how to refer to the LAST element of an array in the query.

Unfortunately, it is impossible as of MongoDB 2.4. The simplest way to implement this feature is to use negative value to point to an element in an array like 'notifications.-1.datetime', but it doesn't work. (Refer to [#SERVER-5565] Handle negative array offsets consistently - MongoDB.)

To make matters worse, it also seems impossible to solve this using Aggregation Framework. There is no way to add an array index to each element when $unwinding ([#SERVER-4588] aggregation: add option to $unwind to emit array index - MongoDB) or select the index of an array dynamically when $projecting. ([#SERVER-4589] aggregation: need an array indexing operator - MongoDB)

Therefore, the only option you have seem to change the schema to match what you want. The simplest way is to add to Order one more field which contains datetime of the last Notification.

Update:

You can first get all candidates from the server, and then narrow down them on the client side to get the final result set. This involves no schema change. If the scale of database is relatively small or some degradation of performance is acceptable, this might be the best solution.

query = Order.where(status: 'pending').elem_match(
  notifications: { datetime: { '$gte' => 10.days.ago, '$lte' => 5.days.ago } })
query.select do |order|
  # datetime = order.notifications[0].datetime
  datetime = order.notifications[order.notifications.size - 1].datetime
  10.days.ago <= datetime && datetime <= 5.days.ago
end.each do |order|
  p order # result
end

Upvotes: 2

Related Questions