Mongoid greater than date or not nil - ruby-on-rails

I have two models
class Conversation
include Mongoid::Document
field :last_moderated_at, type: DateTime
has_many :messages
end
class Message
include Mongoid::Document
include Mongoid::Timestamps
end
I want to get the list of all messages that were created after the moderation date, or all of them if the moderation date is nil
I had expected the following to work
conversation.messages.where(
:created_at.gte => conversation.last_moderated_at
)
But apparently the comparison with nil fails (when last_moderated_at == nil)
Do I have no choice but to use an if/else or is there a mongoDB operator of type greater than that also works when compared to nil dates ?
EDIT : A simpler example : Conversation.where(:created_at.gte => nil).count will always return 0

The syntax is correct. It does work on Mongoid version 6, at least.

Related

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

Rails 4.2.5, Mongoid 5.1.0
I have three models - Mailbox, Communication, and Message.
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
I'm using the authentication gem devise, which gives access to the current_user helper, which points at the current user logged in.
I have built a query for a controller that satisfied the following conditions:
Get the current_user's mailbox, whose communication's are filtered by the box field, where box == 'inbox'.
It was constructed like this (and is working):
current_user.mailbox.communications.where(:box => 'inbox')
My issue arrises when I try to build upon this query. I wish to chain queries so that I only obtain messages whose last message is not from the current_user. I am aware of the .last method, which returns the most recent record. I have come up with the following query but cannot understand what would need to be adjusted in order to make it work:
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
This query produces the following result:
undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>
I am currently able to accomplish this by doing the following, which I know is very inefficient and want to change immediately:
mb = current_user.mailbox.communications.inbox
comms = mb.reject {|c| c.messages.last.from_user == current_user}
I wish to move this logic from ruby to the actual database query. Thank you in advance to anyone who assists me with this, and please let me know if anymore information is helpful here.
Ok, so what's happening here is kind of messy, and has to do with how smart Mongoid is actually able to be when doing associations.
Specifically how queries are constructed when 'crossing' between two associations.
In the case of your first query:
current_user.mailbox.communications.where(:box => 'inbox')
That's cool with mongoid, because that actually just desugars into really 2 db calls:
Get the current mailbox for the user
Mongoid builds a criteria directly against the communication collection, with a where statement saying: use the mailbox id from item 1, and filter to box = inbox.
Now when we get to your next query,
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
Is when Mongoid starts to be confused.
Here's the main issue: When you use 'where' you are querying the collection you are on. You won't cross associations.
What the where(:messages.last.from_user => {'$ne' => current_user}) is actually doing is not checking the messages association. What Mongoid is actually doing is searching the communication document for a property that would have a JSON path similar to: communication['messages']['last']['from_user'].
Now that you know why, you can get at what you want, but it's going to require a little more sweat than the equivalent ActiveRecord work.
Here's more of the way you can get at what you want:
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
I'm not sure my code is 100% (you probably need to check the field names in the documents to make sure I'm searching through the right ones), but I think you get the general pattern.

How to find embedded documents with nil field values in Mongoid?

Is there a way to find nil values inside the embedded documents in Mongoid?
Given I have these models:
class Record
include Mongoid::Document
embeds_many :locations
end
class Location
include Mongoid::Document
embedded_in :record
field :special_id, type: String
end
I can find records given a specific special_id of location model
Record.where('locations.special_id' => '123')
But, if I wanted to get all records with nil special_id in locations, this works but returns all of the records.
Record.where('locations.special_id.eq' => nil)
This one returns 0 results:
Record.where('locations.special_id.exists' => false)
Thanks
embeds_many just sets up an array of hashes inside MongoDB so the usual dot-notation should work:
Record.where('locations.special_id' => nil)
# ------------^^^^^^^^^^^^^^^^^^^^ just the usual JavaScript-style path
The .eq and .exists notations are generally used as methods on symbols as short forms. For example:
where(:'locations.special_id'.ne => nil)
would be a shorthand for:
where('locations.special_id' => { :$ne => nil })
If you try to embed the operator in the field name string then MongoDB will just think you're trying to use another component in the path.

Nested Querying in Mongoid in 2013

So this question is two years old:
Querying embedded objects in Mongoid/rails 3 ("Lower than", Min operators and sorting)
and the way it recommends to query nested objects with less than or greater than:
current_user.trips.where('start.time' => {'$gte' => Time.now}).count
simply doesn't work, it returns 0 for the numerous queries I have like this which is wrong. I've also tried
current_user.trips.where(:'start.time'.gte => Time.now}).count
which is also 0. None of these actually throw an error.
What is the correct syntax for querying nested elements nowadays? Seems to be a fair bit of confusion over this.
It works as you expect in my environment. (mongoid 3.1.3)
class User
include Mongoid::Document
embeds_many :trips
end
class Trip
include Mongoid::Document
embeds_one :start
embedded_in :user
end
class Start
include Mongoid::Document
field :time, type: DateTime
embedded_in :trip
end
User.create({ trips: [
Trip.new({ start: Start.new({ time: 5.days.ago }) }),
Trip.new({ start: Start.new({ time: 2.days.from_now }) })
] })
current_user = User.where({}).first
p current_user.trips.where('start.time' => {'$gte' => Time.now}).count
p current_user.trips.where(:'start.time'.gte => Time.now).count
The above code outputs the following:
1
1
Is $gte really correct? It is a common mistake to use the opposite sign when comparing dates.
Or it might be because you are using older version of Mongoid.
Update:
You can check queries Mongoid generates with the following code:
Mongoid.logger.level = Logger::DEBUG
Moped.logger.level = Logger::DEBUG
Mongoid.logger = Logger.new($stdout)
Moped.logger = Logger.new($stdout)
This is useful for debugging.

Extract Mongoid documents based on the DateTime of their last has_many relations?

I have a bunch of orders, and some of them have order_confirmations.
1: I wish to extract a list of orders based on the DateTime of its last order_confirmation. This is my failed attempt (returns 0 records):
Order.where(:order_confirmations.exists => true).desc("order_confirmations.last.datetime")
2: I wish to extract a list of orders where the last order_confirmation is between 5 and 10 days old. This is my failed attempt (returns 0 results):
Order.lte("order_confirmations.last.datetime" => 5.days.ago).gte("order_confirmations.last.datetime" => 10.days.ago)
My relations:
class Order
include Mongoid::Document
has_many :order_confirmations
end
class OrderConfirmation
include Mongoid::Document
field :datetime, type: DateTime
belongs_to :order
end
With referenced relationships, you cannot directly query referenced documents.
That said, you would probably want to query order confirmations first, and then select the orders like this:
OrderConfirmation.between(datetime: 10.days.ago..5.days.ago)
.distinct(:order_id).map { |id| Order.find(id) }
If you had confirmations embedded into the order, like this
class Order
include Mongoid::Document
embeds_many :order_confirmations
end
class OrderConfirmation
include Mongoid::Document
field :datetime, type: DateTime
embedded_in :order
end
Then you could query order confirmation inside order query with $elemMatch:
Order.elem_match(order_confirmations:
{ :datetime.gte => 10.days.ago, :datetime.lte => 5.days.ago })
Regarding your first question, I don't think it's possible to do that with just MongoDB queries, so you could do something like
# if you go embedded rels
Order.all.map { |o| o.order_confirmations.desc(:datetime).first }
.sort_by(&:datetime).map(&:order)
# if you stay on referenced rels
OrderConfirmation.desc(:datetime).group_by(&:order)
.map { |k, v| v.first }.map(&:order)
Check out the elemMatch function.
where('$elemMatch' => [{...}]
I do believe there is a bug in mongoid though related to elemMatch and comparing dates, not sure if its been fixed.

How do you find all records with a field array count greater than a certain amount in Mongoid?

I basically have a User object and defined inside of it is an array field phone_numbers. I want to get all of the user objects in my database that have a phone_numbers count greater than 1.
Is there a resource to learn about queries like this? I couldn't find anything useful online as most of the results were for more simply queries.
Code:
class Location
include Mongoid::Document
field :name
field :phone_numbers, type: Array
end
I tried the following but it didn't work:
Location.where("this.phone_numbers.count < 1").count
Thanks!
This query search if exists any object in the phone_numbers[0] field
Location.where(:"phone_numbers.0".exists => true).count
This query solve your problem getting the count of phone numbers greater than 1; if exist the field phone_numbers[0] it means at least phone number in the array
Will not be able to check this information by query...create a field in your document which will keep the count of phone_numbers array, update the value you save the document everytime like this
class Location
include Mongoid::Document
field :name
field :phone_numbers, type: Array
field :phone_number_count, type: Integer
after_save do |location|
if location.phone_numbers.blank?
location.phone_number_count = 0
else
location.phone_number_count = location.phone_numbers.size
end
location.save
end
end
and you can then run the query
Location.where(:phone_number_count.gt => 1).enteries
Try this:
Location.where(:phone_numbers.gte => '1').count

Resources