How can one mongoid model query another? - ruby-on-rails

If I have a model called Product
class Product
include Mongoid::Document
field :product_id
field :brand
field :name
...
belongs_to :store
And then I have a model called Store
class Store
include Mongoid::Document
field :name
field :store_id
...
has_many :products
def featured_products
Products.where(:feature.exists => true).and(store_id: self[:store_id]).asc(:feature).asc(:name)
end
How do I create an accessible #store.featured_products which is the results of this query? Right now I get an error that reads
uninitialized constant Store::Products

Use Product, not Products.

I just stumbled on this looking for something else and although the above answer is correct, and the question is ages old, it is very inefficient for what is being asked. As I stumbled on it, so might others.
The example usage above wished to scope the products relationship to just featured products, so a model such as this would work faster (assumed Mongoid 3.0+):
class Product
include Mongoid::Document
field :product_id
field :brand
field :name
field :feature
...
belongs_to :store
scope :featured, where(:feature.exists => true).asc(:feature).asc(:name)
end
class Store
include Mongoid::Document
field :name
field :store_id
...
has_many :products
end
Then instead of #store.featured_products, you could call #store.products.featured. Now if you assume Mongoid 2.x was used, as this was 2011, and looking at this then Mongoid 1.x maybe not have had scopes, the same thing could be achieved like this:
class Product
include Mongoid::Document
field :product_id
field :brand
field :name
field :feature
...
belongs_to :store
end
class Store
include Mongoid::Document
field :name
field :store_id
...
has_many :products
def featured_products
self.products.where(:feature.exists => true).asc(:feature).asc(:name)
end
end
Now the reason both of these are more efficient that just a boolean search on the Products collection is that they start with a cursor creation across the _id field. This is indexed and limits the subsequent where query to just the related documents. The difference would be noticeable on most collection sizes, however the bigger the collection grew the more time would be wasted on a boolean of the entire collection.

Related

Editing a has_one association in ActiveAdmin - avoid saving when nothing is entered

I've got a model in which a very small percentage of the objects will have a rather large descriptive text. Trying to keep my database somewhat normalized, I wanted to extract this descriptive text to a separate model, but I'm having trouble creating a sensible workflow in ActiveAdmin.
My models look like this:
class Person < ActiveRecord::Base
has_one :long_description
end
class LongDescription < ActiveRecord::Base
attr_accessible :text, :person_id
belongs_to :person
validates :text, presence: true
end
Currently I've created a form for editing the Person model, looking somewhat like this:
form do |f|
...
f.inputs :for => [
:long_description,
f.object.long_description || LongDescription.new
] do |ld_f|
ld_f.input :text
end
f.actions
end
This works for adding/editing the LongDescription object, but I still have an issue: I'd like to avoid validating/creating the LongDescription object if no text is entered.
Anyone with better ActiveAdmin skills than me know how to achieve this?
Are you using accepts_nested_attributes_for :long_description? If so, you can add a :reject_if option:
class Person < ActiveRecord::Base
has_one :long_description
accepts_nested_attributes_for :long_description, reject_if: proc { |attrs| attrs['text'].blank? }
end
Note that this is a Rails thing, not an ActiveAdmin thing, and so it will simply skip assignment and update/create of the nested object if that attribute is missing.
More here: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

How to migrate from belongs_to, to embedded_in in Mongoid?

If one first build their models with a belong_to and has_many association and then realized they need to move to a embedded_in and embeds_many association, how would one do this without invalidating thousands of records? Need to migrate them somehow.
I am not so sure my solution is right or not. This is something you might try to accomplish it.
Suppose You have models - like this
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
At first step I will create another model that is similar to the Book model above but it's embedded instead of referenced.
#EmbedBook Model
class EmbedBook
include Mongoid::Document
field :title
embedded_in :user
end
#User Model (Update with EmbedBook Model)
class User
include Mongoid::Document
embeds_many :embed_books
has_many :books
end
Then create a Mongoid Migration with something like this for the above example
class ReferenceToEmbed < Mongoid::Migration
def up
User.all.each do |user|
user.books.each do |book|
embed_book = user.embed_books.new
embed_book.title = book.title
embed_book.save
book.destroy
end
end
end
def down
# I am not so sure How to reverse this migration so I am skipping it here
end
end
After running the migration. From here you can see that reference books are embedded, but the name for the embedded model is EmbedBook and model Book is still there
So the next step would be to make model book as embed instead.
class Book
include Mongoid::Document
embedded_in :user
field :title
end
class User
include Mongoid::Document
embeds_many :books
embeds_many :embed_books
end
So the next would be to migrate embedbook type to book type
class EmbedBookToBook < Mongoid::Migration
def up
User.all.each do |user|
user.embed_books.each do |embed_book|
book = user.books.new
book.title = embed_book.title
book.save
embed_book.destroy
end
end
def down
# I am skipping this portion. Since I am not so sure how to migrate back.
end
end
Now If you see Book is changed from referenced to embedded.
You can remove EmbedBook model to make the changing complete.
This is just the suggestion. Try this on your development before trying on production. Since, I think there might be something wrong in my suggestion.
10gen has a couple of articles on data modeling which could be useful:
Data Modeling Considerations for MongoDB Applications
Embedded One-to-Many Relationships
Referenced One-to-Many Relationships
MongoDB Data Modeling and Rails
Remember that there are two limitations in MongoDB when it comes to embedding:
the document size-limit is 16MB - this implies a max number of embedded documents, even if you just embed their object-id
if you ever want to search across all embedded documents from the top-level, then don't embed, but use referenced documents instead!
Try these steps:
In User model leave the has_many :books relation, and add the
embedded relation with a different name to not override the books
method.
class User
include Mongoid::Document
has_many :books
embeds_many :embedded_books, :class_name => "Book"
end
Now if you call the embedded_books method from a User instance
mongoid should return an empty array.
Without adding any embedded relation to Book model, write your own
migration script:
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
belongs_to :user
def self.migrate
attributes_to_migrate = ["title","price"] # Use strings not symbols,
# we keep only what we need.
# We skip :user_id field because
# is a field related to belongs_to association.
Book.all.each do |book|
attrs = book.attributes.slice(*attributes_to_migrate)
user = book.user // through belong_to association
user.embedded_book.create!(attrs)
end
end
end
Calling Book.migrate you should have all the Books copied inside each user who was
associated with belongs_to relation.
Now you can remove the has_many and belongs_to relations, and
finally switch to clean embedded solution.
class User
include Mongoid::Document
embeds_many :books
end
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
embedded_in :user
end
I have not tested this solution, but theoretically should work, let me know.
I have a much shorter concise answer:
Let's assume that you have the same models:
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
So change it to embeds:
#User Model
class User
include Mongoid::Document
embeds_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
embedded_in :user
end
And generate a mongoid migration like this:
class EmbedBooks < Mongoid::Migration
##attributes_to_migrate = [:title]
def self.up
Book.unscoped.where(:user_id.ne => nil).all.each do |book|
user = User.find book[:user_id]
if user
attrs = book.attributes.slice(*##attributes_to_migrate)
user.books.create! attrs
end
end
end
def self.down
User.unscoped.all.each do |user|
user.books.each do |book|
attrs = ##attributes_to_migrate.reduce({}) do |sym,attr|
sym[attr] = book[attr]
sym
end
attrs[:user] = user
Book.find_or_create_by(**attrs)
end
end
end
end
This works because when you query from class level, it is looking for the top level collection (which still exists even if you change your relations), and the book[:user_id] is a trick to access the document attribute instead of autogenerated methods which also exists as you have not done anything to delete them.
So there you have it, a simple migration from relational to embedded

include one attribute of other collection in mongoid rails Query

I want to query with mongoid , I have following models
class Score
include Mongoid::Document
field :value, :type => Integer
belongs_to :user
end
class User
include Mongoid::Document
field :name, :type => String
field :age, :type => Integer
has_many :scores
I want to query all the scores from collection with their users. but users object should have only have the 'name' field in it
I would be something like
Score.find.all.includes(:user).only(:name)
Please tell the correct syntax for this
Score references User, so it is not possible to retrieve them both in one query, because there are no joins in MongoDB.
Either denormalize and include user name in Score or do several queries.

embeds_many and embeds_one from same model with Mongoid

I have two models, Blog and Theme. A Blog embeds_many :themes and Theme embedded_in :blog. I also have Blog embeds_one :theme (for the activated theme). This does not work. When creating a theme with blog.themes.create it's not stored. If I change the collections so they're not embedded everything works.
# This does NOT work!
class Blog
embeds_many :themes
embeds_one :theme
end
class Theme
embedded_in :blog
end
BUT
# This DOES work!
class Blog
has_many :themes
has_one :theme
end
class Theme
belongs_to :blog
end
Anyone know why this is?
UPDATE
Also there is a problem with assigning one of themes to (selected) theme.
blog.themes = [theme_1, theme_2]
blog.save!
blog.theme = blog.themes.first
blog.save!
blog.reload
blog.theme # returns nil
With this approach you'll embed the same document twice: once in the themes collection and then in the selected theme.
I'd recommend removing the second relationship and use a string attribute to store the current theme name. You can do something like:
class Blog
include Mongoid::Document
field :current_theme_name, type: String
embeds_many :themes
def current_theme
themes.find_by(name: current_theme_name)
end
end
class Theme
include Mongoid::Document
field :name, type: String
embedded_in :blog
end
Note that mongoid embeded documents are initialized at the same time that the main document and doesn't require extra queries.
OK, so I had the same problem and think I have just stumbled across the solution (I was checking out the code for the Metadata on relations).
Try this:
class Blog
embeds_many :themes, :as => :themes_collection, :class_name => "Theme"
embeds_one :theme, :as => :theme_item, :class_name => "Theme"
end
class Theme
embedded_in :themes_collection, :polymorphic => true
embedded_in :theme_item, :polymorphic => true
end
What I have discerned guessed is that:
the first param (e.g. :themes) actually becomes the method name.
:as forges the actual relationship, hence the need for them to match in both classes.
:class_name seems pretty obvious, the class used to actually serialise the data.
Hope this helps - I am obviously not an expert on the inner workings on mongoid, but this should be enough to get you running. My tests are now green and the data is serialising as expected.
Remove embeds_one :theme and instead put its getter and setter methods in Blog class:
def theme
themes.where(active: true).first
end
def theme=(thm)
theme.set(active: false)
thm.set(active: true)
end
There is no need to call blog.save! after blog.theme = blog.themes.first because set performs an atomic operation.
Also, don't forget to add field :active, type: Boolean, default: false in your Theme model.
Hope this works with you.

Tricky extending of mongoid model

Having two models: Car (e.g Audi, Mercedes) and Option (ABS, Laser Headlights, Night Vision ...). Car habtm options.
Suppose both for Audi and Mercedes "Night Vision" option is available.
But to have it in Mercedes you need to pay some extra money for this option. So as I'm guessing I need to extend somehow my Option model to store options's extra price for some cars. Car model I think should also be modified. But can't imagine how.
My aim to achieve behaviour something like this:
Audi.options.night_vision.extra_price => nil
Mercedes.options.night_vision.extra_price => 300
Of course I don't want to duplicate "Night Vision" option in options collection for every car.
Thanks.
This is neither the simplest, or most elegant, just an idea that should work based on assumptions on how you may have implemented the options. Please come back to me if you need more help.
To achieve audi.options.night_vision.extra_price I assume that you have a models such as:
class car
include Mongoid::Document
field :name, type: String
has_and_belongs_to_many :options do
def night_vision
#target.find_by(name:'night_vision')
end
end
end
class option
include Mongoid::Document
field :name, type: String
field :extra_price, type: Float
has_and_belongs_to_many :cars
end
This would enable you to do:
audi = Car.find_by(name:'audi')
audi.options.night_vision.extra_price
If the above assumptions are correct, you should be able to mod your classes like so:
class option
include Mongoid::Document
attr_accessor :extra_price
field :name, type: String
has_and_belongs_to_many :cars
embeds_many :extras
end
class extra
include Mongoid::Document
field :car_id, type: String
field :price, type: String
embedded_in :option
end
class car
include Mongoid::Document
field :name, type: String
has_and_belongs_to_many :options do
def night_vision
extra = #target.find_by(name:'night_vision')
extra_price = extra.prices.find_by(car_id: #target._id.to_s) if extra
extra.extra_price = extra_price.price if extra && extra_price
return extra
end
def find_or_create_option(args)
extra = #target.find_or_create_by(name:args)
price = extra.extras.find_or_create_by(car_id:#target._id.to_s)
price.set(price: args.price
end
end
end
This should then enable you to populate your options like:
audi.options.find_or_create_option({name:'night_vision', price:2310.30})
bmw.options.find_or_create_option({name:'night_vision', price:1840.99})
audi.options.night_vision.extra_price
=> 2310.30
bmw.options.night_vision.extra_price
=> 1840.99
And if you attempted to find night_vision on a car that did not have night_vision you would get:
skoda.options.night_vision
=> nil
skoda.options.night_vision.extra_price
=> NoMethodError (undefined method 'extra_price' for nil:NilClass)

Resources