Mongoid - field with array of references - ruby-on-rails

Im newbee in mongoid and I have two basic (I think) questions. Whats best way to store array of references in Mongoid. Here is a short example what I need (simple voting):
{
"_id" : ObjectId("postid"),
"title": "Dummy title",
"text": "Dummy text",
"positive_voters": [{"_id": ObjectId("user1id")}, "_id": ObjectId("user2id")],
"negative_voters": [{"_id": ObjectId("user3id")}]
}
And its a right way?
class Person
include Mongoid::Document
field :title, type: String
field :text, type: String
embeds_many :users, as: :positive_voters
embeds_many :users, as: :negative_voters
end
Or its wrong?
Also Im not sure, maybe its a bad document structure for this situation? How can i gracefully get if user already voted and dont allow users vote twice? Maybe I should use another structure of document?

Rather than embeds_many you can go for has_many because you just want to save the reference of voters in the document rather than saving whole user document in person
class Person
include Mongoid::Document
field :title, type: String
field :text, type: String
has_many :users, as: :positive_voters
has_many :users, as: :negative_voters
validate :unique_user
def unique_user
return self.positive_voter_ids.include?(new_user.id) || self.negative_voter_ids.include?(new_user.id)
end
end

Related

Mongoid Polymorphic Association Rails

Work env: Rails 4.2 mongoid 5.1
Below are my models:
class Tag
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
belongs_to :entity_tags, :polymorphic => true
end
class EntityTag
include Mongoid::Document
include Mongoid::Timestamps
field :tag_id, type: String
field :entity_id, type: String // Entity could be Look or Article
field :entity_type, type: String // Entity could be Look or Article
field :score, type: Float
end
class Look
include Mongoid::Document
include Mongoid::Timestamps
has_many :tags, :as => :entity_tags
end
class Article
include Mongoid::Document
include Mongoid::Timestamps
has_many :tags, :as => :entity_tags
end
We are trying to implement polymorphic functionality between Looks and Articles to Tags.
i.e. Let's say we have a Tag named "politics", and we would like to add the tag to an Article with the score '0.9' and to a Look with the score '0.6'. The Score should be saved at the EntityTags Model.
The problem:
The first assign of the tag works, but then when I try to assign the same tag to another entity, it removes it and reassigns it from the first one to the latter.
The assignment looks like the following:
entity.tags << tag
Does anybody know the proper way to save associations and create the EntityTag Object with the correct polymorphism and assignment properly?
Thanks!
I've managed to implement a non-elegant working solution based on the following answer in this link

Mongoid::Errors::MixedRelations in AnswersController#create

Getting following error msg while saving an answer:
Problem: Referencing a(n) Answer document from the User document via a relational association is not allowed since the Answer is embedded. Summary: In order to properly access a(n) Answer from User the reference would need to go through the root document of Answer. In a simple case this would require Mongoid to store an extra foreign key for the root, in more complex cases where Answer is multiple levels deep a key would need to be stored for each parent up the hierarchy. Resolution: Consider not embedding Answer, or do the key storage and access in a custom manner in the application code.
Above error is due to the code #answer.user = current_user in AnswersController.
I want to save the login username to the answer which is embaded in question.
deivse User model:
class User
include Mongoid::Document
has_many :questions
has_many :answers
class Question
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Slug
field :title, type: String
slug :title
field :description, type: String
field :starred, type: Boolean
validates :title, :presence => true, :length => { :minimum => 20, :allow_blank => false }
embeds_many :comments
embeds_many :answers
#validates_presence_of :comments
belongs_to :user
end
class Answer
include Mongoid::Document
include Mongoid::Timestamps
field :content, type: String
validates :content, :presence => true, :allow_blank => false
embedded_in :question, :inverse_of => :answers
#validates_presence_of :comments
belongs_to :user
end
class AnswersController < ApplicationController
def create
#question = Question.find(params[:question_id])
#answer = #question.answers.create(params[:answer].permit(:answerer, :content))
#answer.user = current_user
redirect_to #question, :notice => "Answer added!"
end
end
Using Rails 4, Ruby 2.2.2, Mongoid.
That's exactly what the error message says.
Your Answer model is embedded in the question model. That is to say, you can only perform "normal" queries on the Question documents, and not on the models embedded in this one (actually you can, but it's more difficult and somehow kills the point of using embedded documents).
So you can get the user for a given answer, but not the inverse, which you have declared in your user model.
The simplest solution is to remove has_many :answers from the user model, but if you want to retrieve the list of answers for a given user, then embedding models is probably not the best solution: you should have relational models.
To make things clear, you should write belongs_to :user, inverse_of: nil

Sorting a Mongoid Object via its Associated Model's Attribute

I'm having some problems trying to understand how Mongoid does its sorting. I have 2 models, Gig and Venue, both of which are associated by a belong_to has_many relationship.
I'm trying to sort objects from Gig by the attribute 'name' of the Venue Object to no avail.
I'm hoping someone out there would be able to point me to the right direction.
Below are a truncated model description.
My Query is also below:
# Gig Model
class Gig
include Mongoid::Document
include Mongoid::Paperclip
include SearchMagic
belongs_to :owner, :class_name => "User", :inverse_of => :owns
belongs_to :venue
has_and_belongs_to_many :attenders, :class_name => "User", :inverse_of => :attending
has_and_belongs_to_many :artistes
<snip>
end
# Venue Model
class Venue
include Mongoid::Document
include Mongoid::Paperclip
include SearchMagic
has_many :gigs
field :foursquare_id, type: String
embeds_one :address
embeds_many :user_ratings
field :facebook, type: String
field :twitter, type: String
field :website, type: String
field :name, type: String
field :postal, type: String
field :tel, type: String
field :venue_type, type: String
field :description, type: String
field :rating, type: Float, default: 0.0
<snip>
end
# Console
>> Gig.desc('venue.name').map{|f| f.venue.name}
=> ["*SCAPE", "Velvet Underground", "Blujaz Lounge", "Velvet Underground", "Home Club", "Wh
ite House, Emily Hill", "Zouk", "Zouk", "The Pigeonhole", "Home Club", "Home Club", "Home C
lub"]
# sorting doesn't work
You can't join in mongo. If you need joins, use a relational database. A "feature" of non-relational databases is that you can't do joins.
You have basically two choices:
a before_save callback, which will inject the name of the venue into the gig as an additional field (see for instance https://github.com/rewritten/timebank/blob/master/lib/mongoid/denormalize.rb)
a map-reduce task, which after any modification of any venue or gig, will update the venue name into the gig as an additional field.
In the end, you need a field in the Gig collection to order it.

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