Mongoid: inconsistent results on Query when using $elemMatch for nested arrays - ruby-on-rails

Context
puts [
"#{RUBY_ENGINE} #{RUBY_VERSION}",
"Rails gem: #{Rails.gem_version.version}",
"MongoDB #{Mongoid.default_client.command(buildInfo: 1).first[:version]}",
"Mongoid gem: #{Mongoid::VERSION}"
].join("\n")
ruby 2.7.6
Rails gem: 5.2.4.5
MongoDB 4.4.18
Mongoid gem: 7.5.1
Scenario
To this relationships: Foo#bars, Bar#bazes, these are the cases aimed for:
Identify Bar documents where any label of bazes matches cheese.
Identify Bar documents where any id of bazes matches a target id.
Identify Bar documents where bazes have any of an array of ids.
Although I could work out (1), cases (2) and (3) remain a mystery. Below an example, where in Queries and Results there is some example.
Example
Data Model
class Baz
include Mongoid::Document
embedded_in :bar
field :label, type: String
end
class Bar
include Mongoid::Document
embedded_in :foo
embeds_many :bazes, class_name: "Baz"
end
class Foo
include Mongoid::Document
field :name, type: String
embeds_many :bars, class_name: "Bar"
end
Data Sample
foo = Foo.create!({
name: "foo",
bars: [
{bazes: [{label: "table"}, {label: "chair"}]},
{bazes: [{label: "bread"}, {label: "cheese"}]},
{bazes: [{label: "up"}, {label: "down"}]},
{bazes: [{label: "high"}]},
{bazes: [{label: "low"}]}
]
})
Queries and Results
Select the target for cases (1) and (2):
baz = foo.bars[1].bazes.last
baz.label
#=> "cheese"
baz.id
#=> BSON::ObjectId('638d3b824b9f26049b42ac0a')
Case 1
foo.bars.where(bazes: {"$elemMatch": {label: baz.label}}).count
#=> 1
It seems to be working properly. Doing some double check:
bazes = foo.bars.where(bazes: {"$elemMatch": {label: baz.label}}).pluck(:bazes).first
found = bazes.where(label: baz.label).first
#=> #<Baz _id: 638d3b824b9f26049b42ac0a, label: "cheese">
found == baz
#=> true
It certainly worked.
Case 2
Using exactly the same syntax as in Case (1), but matching the id field instead:
foo.bars.where(bazes: {"$elemMatch": {id: baz.id}}).count
#=> 0
It does not work.
At this point I am quite lost. How would a field like label work but the id wouldn't?
Case 3
The below is what I guess it should look like. It doesn't work either, although it makes sense, given that the base case (2) above fails in the first place...
foo.bars.where(bazes: {"$elemMatch": {:id.in => [baz.id]}}).count
#=> 0

Related

ActiveRecord group not returning hash with arrays of objects for values

I know this has been asked and answered a lot. I have two tables Foo and Bar.
class Foo < ApplicationRecord
belongs_to :bar
...
Foo has attributes of id and name and bar_id
and
class Bar < ApplicationRecord
has_many :foos
...
Bar has the attributes, id and name.
When I simply try Foo.group(:bar_id) I get #<Foo::ActiveRecord_Relation:0x3fdeac1cc274>
With Foo.group(:bar_id).count I get {5=>2, 1=>2} the keys being the bar_id and the values the count of how many have that id.
What I'm trying to do is, group Foo on Bar#name with an array of Foos as the values.
{
'name1' => [#<Foo:0x00007fbd5894f698 id:1, name: 'thing'...}, ...],
'name2' => [#<Foo:0x00017fbd5894f698 id:5, name: 'thing'...}, ...],
...
}
With Foo.joins(:bar).group('bars.name').count I am able to return {"name1"=>2, "name2"=>2} But not an array of the Foo models. I know it's because of the count. But without the count it simply returns #<Foo::ActiveRecord_Relation:0x3fdeac1cc274>
I see a lot of suggestions using Enumerable#group_by. I don't want to use an enumerable as I'm using ActiveRecord and as the records increase, it will drastically slow down the look up.
I've noticed that you're using PostgreSQL. Why not then use the json aggregation functions. It a bit differs from your desired result, but still contains the same information:
Bar
.joins(:foos)
.group("bars.name")
.pluck("bars.name", "json_agg(json_build_object('id', foos.id, 'name', foos.name))")
.to_h
The result is going to be:
{
'name1' => [{id:1, name: 'thing'}, ...],
'name2' => [{id:5, name: 'thing'}, ...],
...
}

what does where(:genres.in => [ "rock" ] mean in Mongoid and how can I find the reference?

anyone who knows what does where(:genres.in => [ "rock" ] mean in mongoid?
I saw these codes in documentation:
class Band
include Mongoid::Document
field :country, type: String
field :genres, type: Array
scope :english, ->{ where(country: "England") }
scope :rock, ->{ where(:genres.in => [ "rock" ]) }
end
https://docs.mongodb.com/mongoid/master/tutorials/mongoid-queries/#named-scopes
Seems it means that find the document where genres contains "rock" but I'm not sure for I can't find a reference explain the in.
This is a small ruby DSL that pretty much directly maps to mongodb query operators. That is, this ruby line:
Band.where(:genres.in => [ "rock" ])
will translate to this mongodb query (using shell syntax here)
db.bands.find({ genres: { $in: ['rock']}})
Same for age.gt, etc. Here's the list of underlying mongodb query operators: https://docs.mongodb.com/manual/reference/operator/query/#query-selectors

Rails 4: select multiple attributes from a model instance

How do I fetch multiple attributes from a model instance, e.g.
Resource.first.attributes(:foo, :bar, :baz)
# or
Resource.where(foo: 1).fetch(:foo, :bar, :baz)
rather than returning all the attributes and selecting them manually.
You will use the method slice.
Slice a hash to include only the given keys. Returns a hash containing the given keys.
Your code will be.
Resource.first.attributes.slice("foo", "bar", "baz")
# with .where
Resource.where(foo: 1).select("foo, bar, baz").map(&:attributes)
How about pluck:
Resource.where(something: 1).pluck(:foo, :bar, :baz)
Which translates to the following SQL:
SELECT "resources"."foo", "resources"."bar" FROM, "resources"."baz" FROM "resources"
And returns an array of the specified column values for each of the records in the relation:
[["anc", 1, "M2JjZGY"], ["Idk", 2, "ZTc1NjY"]]
http://guides.rubyonrails.org/active_record_querying.html#pluck
Couple of notes:
Multiple value pluck is supported starting from Rails 4, so if you're using Rails 3 it won't work.
pluck is defined on ActiveRelation, not on a single instnce.
If you want the result to be a hash of attribute name => value for each record you can zip the results by doing something like the following:
attrs = [:foo, :bar, :baz]
Resource.where(something: 1).pluck(*attrs).map{ |vals| attrs.zip(vals).to_h }
To Fetch Multiple has_one or belongs_to Relationships, Not Just Static Attributes.
To fetch multiple relationships, such as has_one or belongs_to, you can use slice directly on the instance, use values to obtain just the values and then manipulate them with a map or collect.
For example, to get the category and author of a book, you could do something like this:
book.slice( :category, :author ).values
#=> #<Category id: 1, name: "Science Fiction", ...>, #<Author id: 1, name: "Aldous Huxley", ...>
If you want to show the String values of these, you could use to_s, like:
book.slice( :category, :author ).values.map( &:to_s )
#=> [ "Science Fiction", "Aldous Huxley" ]
And you can further manipulate them using a join, like:
book.slice( :category, :author ).values.map( &:to_s ).join( "➝" )
#=> "Science Fiction ➝ Aldous Huxley"

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.

Mongoid: select embedded objects that fits number of options

I have got this structure
class House
include Mongoid::Document
embeds_many :inhabitants
end
class Inhabitant
include Mongoid::Document
embedded_in :house
field :name
field :gender
field :age
end
I can get all houses where females live:
houses = House.where("inhabitants.gender" => "female")
But how can I get all houses where females under age 50 live? How can I specify more than one condition for embedded object?
To apply multiple conditions to each entry in an array, you should use the $elemMatch operator. I'm not familiar with Mongoid, but here's the MongoDB shell syntax for your query modified to use $elemMatch:
> db.house.find({inhabitants: {$elemMatch: {gender: "female", age: {$lt: 50}}}})
Try this:
houses = House.where("inhabitants.gender" => "female", "inhabitants.age" => {"$lt" => 50})
Combining conditions:
MongoDB query:
db.houses.find({'inhabitants.age' : {$in: {$lt: 50}}})
Mongoid:
houses = House.where('inhabitants.age' => {'$in' => {'$lt' => 50}})

Resources