Mongoid/MongoDB: Order a query by the value of an embedded document? - mongodb

I am attempting to order the results of a query by the value of a specific embedded document, but even with what seems to be a valid set of options and using the $elemMatch operator, my results are coming back in natural order.
My model is composed of Cards, which embeds_many :card_attributes, which in turn reference a specific CardAttributeField and contain an Integer value. I would like to be able to order a collection of Cards by that value.
I am able to isolate a collection of Cards which have a CardAttribute referencing a specific CardAttributeField like this:
cards = Card.where(:card_attributes.elem_match => {
:card_attribute_field_id => card_attribute_field.id
})
If I knew the order in which the card_attributes were set, I could use MongoDB array notation, like this:
cards.order_by(['card_attributes.0.value', :asc])
This does deliver my expected results in test scenarios, but it won't work in the real world.
After much messing around, I found a syntax which I thought would allow me to match a field without using array notation:
cards.asc(:'card_attributes.value'.elem_match => {
:card_attribute_field_id => card_attribute_field.id
})
This produced a set of options on the resulting Mongoid::Criteria which looked like:
{:sort=>{"{#<Origin::Key:0x2b897548 #expanded=nil, #operator=\"$elemMatch\", #name=:\"card_attributes.value\", #strategy=:__override__, #block=nil>=>{:card_attribute_field_id=>\"54c6c6fe2617f55611000068\"}}"=>1}}
However, the results here came back in the same order regardless or whether I called asc() or desc().
Is there any way to do what I'm after? Am I taking the wrong approach, or do I have a mistake in my implementation? Thanks.
Simplified, my model is:
class Card
include Mongoid::Document
# various other fields
has_many :card_attribute_fields
embeds_many :card_attributes do
def for_attribute_field card_attribute_field
where(:card_attribute_field_id => card_attribute_field.id)
end
end
end
class CardAttributeField
include Mongoid::Document
belongs_to :card
field :name, type: String
field :default_value, type: String
field :description, type: String
end
class CardAttribute
include Mongoid::Document
embedded_in :card
field :card_attribute_field_id, type: Moped::BSON::ObjectId
field :value, type: Integer
end

Related

RAILS Mongoid finding document by id

When I do in rails console:
User.first
=> #<User _id: 6241f97de64d1eb2cc003d08,....
I get document, but when I try to find it by id like
User.find('6241f97de64d1eb2cc003d08') or User.find(BSON::ObjectId('6241f97de64d1eb2cc003d08')) or
User.find('6241f97de64d1eb2cc003d08'.to_bson) etc
I get:
message:
Document(s) not found for class User with id(s) 6241f97de64d1eb2cc003d08.
also
3.0.3 :049 > User.where(_id: BSON::ObjectId('6241f97de64d1eb2cc003d08')).find
=> nil
Why it does not work????
I use rails 7 and mongoid
Finally found the solution.
User model was defined like this:
class User
include Mongoid::Document
include Mongoid::Timestamps
field :_id, type: String
field :first_name, type: String
field :last_name, type: String
end
And changing it to this:
class User
include Mongoid::Document
include Mongoid::Timestamps
# field :_id, type: String
field :first_name, type: String
field :last_name, type: String
end
Commenting "field: _id" did the trick. I think string type was "covering" expected ObjectId as _id and find methods are converting strings into ObjectId so we ended up in search against string vs ObjectId.

Mongoid behavior when discriminator key is not found

Looking at mongoid inheritance documentation it says:
Similarly, when querying by parent classes (Canvas in this example), any documents in the collection that do not have a discriminator value, or whose discriminator value does not map to either the parent or any of its descendants, will be returned as instances of the parent class.
Using the example classes on that page:
class Canvas
include Mongoid::Document
field :name, type: String
embeds_many :shapes
end
class Browser < Canvas
field :version, type: Integer
scope :recent, ->{ where(:version.gt => 3) }
end
If I insert a document into the Canvas collection like so:
{
"_id": { "$oid": "612d5bd10170cb02ad9bfbac" },
"_type":"SpecialCanvas"
}
And then query for that document like so:
Canvas.find_by(id: '612d5bd10170cb02ad9bfbac')
I get this error:
Mongoid::Errors::UnknownModel:
message:
Attempted to instantiate an object of the unknown Model 'SpecialCanvas'.
summary:
A document with the value 'SpecialCanvas' at the key '_type' was used to instantiate a model object but Mongoid cannot find this Class.
resolution:
The _type field is a reserved one used by Mongoid to determine the class for instantiating an object. Please don't save data in this field or ensure that any values in this field correspond to valid Models.
from /usr/local/bundle/gems/mongoid-7.2.5/lib/mongoid/factory.rb:87:in `rescue in from_db'
Caused by NameError: uninitialized constant SpecialCanvas
from /usr/local/bundle/gems/activesupport-5.2.6/lib/active_support/inflector/methods.rb:283:in `block in constantize'
But, based on the documentation, I would expect it to just return an instance of the parent class (Canvas). Am I misunderstanding this?
Seems like a documentation bug to me. Please report it to https://jira.mongodb.org/browse/MONGOID.

Indexing of JSONB embedded Ecto2 models in Postgres 9.4+

I'm not clear on how to index embedded structs stored as JSONB with Ecto2/Postgres 9.4+
I have a schema with two embedded structs using embeds_one and embeds_many. They are ecto :map fields represented in Postgres as JSONB. I am wondering how I can be sure they are indexed (using Gin?) for speedy queries? I am not sure if this happens automagically, if I need to add an index to my migration or if I need to do it manually using psql etc..
Just looking for clarification on how this works.
Thanks!
defmodule App.Repo.Migrations.CreateClient
def change do
create table(:clients) do
add :name, :string
add :settings, :map
add :roles, {:array, :map}, default: []
timestamps()
end
// This works for normal schema/model fields
create index(:clients, [:name], unique: true, using: :gin)
// BUT CAN I INDEX MY EMBEDS HERE?
// GUESS:
create index(:clients, [:settings], using: :gin)
end
end
defmodule App.Client do
schema "client" do
field :name, :string
embeds_one :settings, Settings // single fixed schema "Settings" model
embeds_many :roles, Role // array of "Role" models
end
end
defmodule Settings do
use Ecto.Model
embedded_schema do // ALSO
field :name, :string // are types relevant?
field :x_count, :integer // stored as strings (INDEXED?)
field :is_active, :boolean // deserialized via cast?
end
end
defmodule Role do
use Ecto.Model
embedded_schema do
field :token
field :display_english
field :display_spanish
end
end
I think you just need to add this:
create index(:clients, [:name], unique: true, using: :gin)
to your migration file.
Or if the index sql statement is gonna be complicated, you could do it with execute so it would be something like this:
execute("CREATE INDEX clients_name_index ON clients USING GIN (name)")
I have not tested it but I believe it should work.

Get all keys in MongoMapper Model including its association

Suppose the following model :
class Product
include MongoMapper::Document
key :name, String
key :product_category_id, ObjectId
belongs_to :product_category
end
class ProductCategory
include MongoMapper::Document
key :name, String, :required => true, :unique => true
timestamps!
userstamps!
end
I want to implement an advanced search that will inspect all value inside my Model including its all association like :
I have :
Product A data named "aLaptop" belongs_to:ProductCategory named "Notebook".
Product B data named "aGreatNotebook" belongs_to:ProductCategory named "Notebook".
When I search with keyword named "Notebook", I want to search it to Product.name fields and also its associations which mean ProductCategory.name also. So it will returned both of that items, because Product A have ProductCategory.name "Notebook" & Product B have Product.name "aGreatNotebook" and ProductCategory "Notebook"..
How can I do this?? I've already searching for 2 days and not success until now:(.. When is in MySQL, I used join table.. But hows in MongoMapper??
Please help.. Thank you..
You can't do joins in MongoDB. So the basic idea is to get the ObjectId associated with the "Notebook" category and then to query the products where product_category is equal to notebook_id. This generally involves two queries. So that'd be something like this:
notebook_id = ProductCategory.first(:name => "Notebook")
if notebook_id
Product.where({:product_category_id => notebook_id['_id']})
end
The question is confusing, but the title of the question is clear.
So in case someone comes here hoping to see how to get all of the:
keys
associations
Read on...
To get the keys in a model:
ConfinedSpace.keys.keys
=> ["_id", "photo_ids", "include_in_qap", "position", "created_at", "updated_at",
"structure_id", "identifier", "name", "description", "notes", "entry_info",
"anchor_points", "nature", "special_equipment", "rescue_overview"]
And to get the associations:
ConfinedSpace.associations.each{|name,assoc| puts name}
photos
attachments
activities
structure
videos
And the class (edited for brevity):
class ConfinedSpace
include MongoMapper::EmbeddedDocument
include Shared::HasPhotos
include Shared::HasAttachments
include HasActivities
TAG = "ConfinedSpace"
belongs_to :structure
many :videos, :as => :attachable
key :identifier, String
key :name, String
key :description, String
key :notes, String
key :entry_info, String
key :anchor_points, String
key :nature, String
key :special_equipment, String
key :rescue_overview, String
validates :identifier, presence: true
end

mongoid - how to query by embedded object

I have the following model:
class User
include Mongoid::Document
store_in :users
field :full_name, :type => String
end
class Message
include Mongoid::Document
embeds_one :sender, :class_name => "User"
field :text, :type => String
end
I would like to store User and Message in separated standalone collections so that they could be queried directly, and I would like to have one copy of user for sender in each Message entry. Is my model correct for this kind of request?
And when I have an instance of User user, how could I query the messages where sender = user?
I've tried:
Message.where(:sender => user)
Message.where('sender.id' => user.id)
both not work.
only Message.where('sender.full_name' => user.full_name) worked, but I don't want to rely on a text field when there's an id field to use.
What's the best way to do that?
How I save Message/User:
user = User.new
user.full_name = 'larry'
user.save
m = Message.new(:text => 'a text message')
m.sender = user
m.save
And it results in the database:
> db.users.find({ 'full_name' : 'larry'})
> db.messages.find({})[0]
{
"_id" : ObjectId("4f66e5c10364392f7ccd4d74"),
"text" : "a text message",
"sender" : {
"_id" : ObjectId("4f62e0af03642b3fb54f82b0"),
"full_name" : "larry"
}
}
Like explain by Jordan Durran ( Mongoid lead developer ) in Google group of Mongoid : http://groups.google.com/group/mongoid/browse_thread/thread/04e06a8582dbeced#
You're going to need a separate model if you want to embed the user
data inside the message. When denormalizing like this I generally
namespace one of them, and create a module with the common fields to
include in both - maybe in your case you can call it Sender?
class Sender
include Mongoid::Document
include UserProperties
class << self
def from_user(user)
Sender.new(user.attributes)
end
end
end
class User
include Mongoid::Document
include UserProperties
end
module UserProperties
extend ActiveSupport::Concern
included do
field :full_name, type: String
end
end
class Message
include Mongoid::Document
embeds_one :sender
end
You also don't need the :store_in macro on User - by default it's name
would be "users".
You can't do what you do.
Your user document is save in his one collection because you use the store_in method. And you try save it on an other document ( Message)
If you really want 2 collections, you need use has_one :user in your Message class.
class Message
has_one :sender, :class_name => 'User', :foreign_key => 'sender_id'
end
After you can get your message like :
Message.senders to have all of your sender.