Retrieve an embedded document using Mongoid - ruby

I have a Mongoid document called Equipment which can embed multiple Question documents. Here are the document schemas:
class Equipment
include Mongoid::Document
include Mongoid::Timestamps
field :description
field :modelNumber
field :title
field :categoryId
field :subCategoryId
field :stateId
field :city
field :askingPrice
embeds_many :questions
end
class Question
include Mongoid::Document
field :content
attr_accessible :content
embedded_in :equipment, :inverse_of => :questions
embeds_one :answers
end
My issue is that I can retrieve a Question document based on the Question Id. However, my current query returns the parent Equipment document. I would have expected the query to return the embedded Question document. I can eventually get the embedded Question document but I have to loop through all the Question documents of the parent Equipment document.
Here is my current query:
#question = Equipment.where('questions._id' => Moped::BSON::ObjectId(params[:id])).first
Is there a way to directly obtain the embedded Question document?

Because you are using embedded documents, grabbing a single question may not make sense. It does, however, make sense in the context of the parent document. If you really just want a specific question, you can just use normal ARish syntax:
question = Question.find(params[:id])
I would strongly suggest you make sure you have an index on the question._id field. This will help ensure this query is a fast read operation. Otherwise, Mongo will need to run through all of the Equipment documents and check each of the embedded Question objects.
If the above doesn't work, you can try a nested relation:
Equipment.find(params[:equipment_id]).questions.find(params[:id])

With rails (3.2.15) and mongoid (3.1.5) I can only do it in this way:
#equipment = Equipment.where('questions._id' => Moped::BSON::ObjectId(params[:id])).first
#question = #equipment.questions.find(params[:id])

Related

How to validate a mongoid field based on another mongoid field?

I made the switch to nosql and am playing with Mongoid with Sinatra. I'm still new to this, and I"m stumped on this problem.
class Item
field :name, type: String
field :category, type: Moped::BSON::ObjectId # holds _id of the category object
field :cover_type, type: String
validates_presence_of :category
validates_inclusion_of :cover_type, in:["Hardcover", "Softcover"]
end
class Category
field :name
validates_inclusion_of :name, in:["Books", "Movies", "Ebooks"]
end
Imagine a store that sells Books, Movies, and Ebooks and each item sold belongs to one of the three categories. If an Item is listed under the category "Books", then the Item is required to have a field called cover_type. Furthermore, cover_type can only be either "Hardcover" or "Softcover".
When saving an Item, how do I piece together the validate in the Item class for when the Item is in the Book category and therefore requires the presence of the field cover_type, which is also validated as being "Hardcover" or "Softcover"?
If the Item isn't a Book, then cover_type can be null.
Mongoid models inherit ActiveModel validations. You can just write a custom validator method:
class Item
validates :cover_type_must_be_valid, if: :book?
def book?
Category.find(category).name == 'Books'
end
def cover_type_must_be_valid
errors.add(:cover_type, 'must be Hardcover or Softcover') unless %w{Hardcover Softcover}.include? cover_type
end
end
The book? method is unpleasant; why not use belongs_to :category on Item and has_many :items on Category?
EDIT: here’s what book? could look like if you used the has_many and belongs_to:
def book?
category.name == 'Books'
end
Not that different, but you’ll surely be accessing item.category all over your application, I can’t see why you wouldn’t want to make it easier.

Mongoid: Retrieves all embedded documents

Suppose we have these models:
class Person
include Mongoid::Document
embeds_many :albums
end
class Album
include Mongoid::Document
embeds_many :photos
end
class Photo
include Mongoid::Document
end
What I want is to retrieves all Photo of a particular Person. Is there a mongoid/mongodb shortcuts or the only way is to iterate over person.albums and store all album.photos in a new array?
Thanks.
You have 2 ways to do this, one is through Mongoid, which, AFAIK, will inflate all objects.
Something like:
Person.only("albums.photos").where(id: '1').albums.map(&:photos).flatten
Or you can do it in Moped(driver) which will return only an array of photos.
Person.collection.find(id: "1").select("albums.photos" => 1).
first["albums"].map { |a| a["photos"] }.flatten
On the DB load, both dont make any difference, as they will yield the same query, only difference is that the first one will create way more objects than the second one.

Querying and sorting embedded document in mongoid

I have three classes
class Post
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :user, :inverse_of => nil
embeds_many :comments, :as => :commentable
field :content, :type => String
end
class Commment
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :user, :inverse_of => nil
embedded_in :commentable, :polymoriphic => true
end
class User
has_many :posts, :dependent => :destroy
field :name, :type => String
end
Whenever the user creates a new comment, I want to compare the contents of it with the latest comment that the user has made. Here is my code that fetches the latest comment by the user:
comments_user=[]
Post.where("comments.user_id" => user.id).
only(:comments).
each {|p| comments_user += p.comments.where(:user_id => user.id).to_a}
latest_comment = comments_user.sort_by{|comment| comment[:updated_at]}.reverse.first
The above code gives me the result but the approach taken is not efficient as I have to traverse through all the posts that the user has commmented to find the latest comment. If any, can anyone provide me a more efficient solution to this problem?
Simply speaking, Isn't there any way I can get all the comments made by this user?
This should fetch the latest user`s comment:
Post.where("comments.user_id" => user.id).order_by(:'comments.updated_at'.desc).limit(1).only(:comments).first
This is standard problem with embedding. It greatly improves some queries ("load post with all its comments"), but makes others non-efficient/impractical ("find latest comment of a user").
I see two options here:
Keep embedding and duplicate data. That is, when user makes a comment, embed this comment to a post document and to the user document. This data duplication has its drawbacks, of course (what if you need to edit comments?);
Stop embedding and start referencing. This means that comment is now a top level entity. You can't quickly load a post with comments, because there are no joins. But other queries are faster now, and there's no data duplication.

Mongoid: Run callback from embedded document on parent

Rails 3.0.1
Mongoid (2.0.0.beta.20)
Class Post
embeds_many :comments
field :comments_count
end
Class Comment
embedded_in :commentable, :inverse_of => :comments
end
I want to select the 10 most commented posts. To do that I need comments_count field in Post. But since my Comment is polymorphic (Post.comments, Message.comments etc) I don't want create inc callbacks in Post. What I wan't to do is create callback in Comment which will update comment_count field in Post.
I don't know how I can perform inc operation in embedded document on Field from parrent document and execute this callback from parrent document
Here is how to increment the Post from the embedded polymorphic Comment:
Class Comment
after_create :update_post_comment_count
def update_post_comment_count
if self._parent.class == Post
Post.collection.update( {'_id' => self._parent._id},
{'$inc' => {'comment_count' => 1}} )
end
end
end
I am pretty sure that this callback will execute whenever a new Comment is created so I don't think you need to worry about executing it from the parent document. Let me know if it works.
See this SO answer and this Github issue for more info on callbacks in embedded documents.

How to reference an embedded document in Mongoid?

Using Mongoid, let's say I have the following classes:
class Map
include Mongoid::Document
embeds_many :locations
end
class Location
include Mongoid::Document
field :x_coord, :type => Integer
field :y_coord, :type => Integer
embedded_in :map, :inverse_of => :locations
end
class Player
include Mongoid::Document
references_one :location
end
As you can see, I'm trying to model a simple game world environment where a map embeds locations, and a player references a single location as their current spot.
Using this approach, I'm getting the following error when I try to reference the "location" attribute of the Player class:
Mongoid::Errors::DocumentNotFound: Document not found for class Location with id(s) xxxxxxxxxxxxxxxxxxx.
My understanding is that this is because the Location document is embedded making it difficult to reference outside the scope of its embedding document (the Map). This makes sense, but how do I model a direct reference to an embedded document?
Because Maps are their own collection, you would need to iterate over every Map collection searching within for the Location your Player is referenced.
You can't access embedded documents directly. You have to enter through the collection and work your way down.
To avoid iterating all of the Maps you can store both the Location reference AND the Map reference in your Player document. This allows you to chain criteria that selects your Map and then the Location within it. You have to code a method on your Player class to handle this.
def location
self.map.locations.find(self.location_id)
end
So, similar to how you answered yourself except you could still store the location_id in your player document instead of using the coord attribs.
Another way would be to put Maps, Locations, and Players in their own collections instead of embedding the Location in your Map collection. Then you could use reference relationships without doing anything fancy... however your really just using a hierarchical database likes it's a relational database at this point...
PLEASE go and VOTE for the "virtual collections" feature on MongoDB's issue tracker:
http://jira.mongodb.org/browse/SERVER-142
It's the 2nd most requested feature, but it still hasn't been scheduled for release. Maybe if enough people vote for it and move it to number one, the MongoDB team will finally implement it.
In my use case, there is no need for the outside object to reference the embedded document. From the mongoid user group, I found the solution: Use referenced_in on the embedded document and NO reference on the outside document.
class Page
include Mongoid::Document
field :title
embeds_many :page_objects
end
class PageObject
include Mongoid::Document
field :type
embedded_in :page, :inverse_of => :page_objects
referenced_in :sprite
end
class Sprite
include Mongoid::Document
field :path, :default => "/images/something.png"
end
header_sprite = Sprite.create(:path => "/images/header.png")
picture_sprte = Sprite.create(:path => "/images/picture.png")
p = Page.create(:title => "Home")
p.page_objects.create(:type => "header", :sprite => header_sprite)
p.page_objects.first.sprite == header_sprite
My current workaround is to do the following:
class Map
include Mongoid::Document
embeds_many :locations
references_many :players, :inverse_of => :map
end
class Player
referenced_in :map
field :x_coord
field :y_coord
def location=(loc)
loc.map.users << self
self.x_coord = loc.x_coord
self.y_coord = loc.y_coord
self.save!
end
def location
self.map.locations.where(:x_coord => self.x_coord).and(:y_coord => self.y_coord).first
end
end
This works, but feels like a kluge.
Thinking outside the box, you could make Location its own document and use Mongoid Alize to automatically generate embedded data in your Map document from your Location documents.
https://github.com/dzello/mongoid_alize
The advantage of this method is that you get efficient queries when conditions are suitable, and slower reference based queries on the original document when there's no other way.

Resources