embeds_many and embeds_one from same model with Mongoid - ruby-on-rails

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.

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

Changing Mongoid class name mid-production

Is this even possible?
I have a mongoid class named Magazine, with some associations as well, that I would like to re-name to Publication. Problem is that I already have a bunch of users who have already made magazines, issues and articles.
Original Magazine model:
class Magazine
# 1. Include mongoid stuff
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Slug
# 2. Define fields
field :title, type: String
field :description, type: String
field :live, type: Boolean, default: false
field :show_walkthrough, type: Boolean, default: true
# 3. Set attributes accesible
attr_accessible :title, :description, :live, :show_walkthrough, :cover_image_attributes, :logo_image_attributes
# 4. Set slug
slug :title
# 5. Set associations
belongs_to :user
has_many :issues, dependent: :delete, autosave: true
has_one :foreword, :as => :articleable, :class_name => 'Article', dependent: :delete, autosave: true
embeds_one :cover_image, :as => :imageable, :class_name => 'Image', cascade_callbacks: true, autobuild: true
embeds_one :logo_image, :as => :imageable, :class_name => 'Image', cascade_callbacks: true, autobuild: true
# 6. Accepting nested attributes
accepts_nested_attributes_for :cover_image, :allow_destroy => true
accepts_nested_attributes_for :logo_image, :allow_destroy => true
# 7. Set validations
validates_presence_of :title, :description, :cover_image, :logo_image
end
I know I can change the class-name to Publication and then do db.magazines.renameCollection( "publications" ) on the mongodb, but the associations doesn't follow along.
Any suggestions?
I looks like you have association fields in your Issue and Foreword models that probably refer to Magazine. So if you are happy enough to change the name of the class and underlying collection then renaming these association fields is your main problem. You may have something like:
class Issue
belongs_to :magazine
end
You could redefine this association as belongs_to :publication. Assuming that are happy to fix all the references to Issue#magazine in your code then your remaining problem is that your issues collection will be full of documents that have a magazine_id field instead of publication_field. You have two options to fix the database mapping.
First option is to rename the field in the database. See mongoDB : renaming column name in collection
The second option is to declare the association so that it maps to the old database field by overriding the 'foreign key' name:
belongs_to :publication, foreign_key: :magazine_id
You will have to repeat this for the Foreword model and any others that reference Magazine.
Just a heads up for polymorphism and class inheritance.
Mongoid handles inheritance and polymorphic associations by storing the class name as a document attribute.
On the class itself, this is stored as the "_type" attribute
For polymorphic associations like belongs_to :polymorphic_class mongoid adds an attribute "polymorphic_class_type", so that the class can be resolved (with Rails' .constantize) when browsing polymorphic associations.
So if you decide to change the class name, and you have inheritance or polymorphic associations, well you'll have to also rewrite all those attributes !

How to configure a pg_search multisearch on associated models in Rails?

I'm adding pg_search into a Rails app. I'm not completely understanding the configuration, and would appreciate a gentle nudge in the right direction.
First, I already have a multi model site more or less set up and running on my app. But I want to extend it to also search on associated models.
For example, I have Manufacturer, Car, Model classes. Currently if I search for "Ford", only the manufacturer is returned. I'd also like to return all the associated Cars (which belong to Manufacturer) and Models (which belong to Car).
I can see how to do this as a scoped search
class Car
pg_search_scope :manufactured_by, :associated_against => {
:manufacturer => [:name]
}
end
But if I try to do this on a multisearch it doesn't work
class Car
include PgSearch
multisearchable :against => [:name],
:associated_against => {
:manufacturer => [:name]
}
end
It doesn't generate an error, it simply doesn't pick up the associated records.
I have a feeling I'm missing something fundamental in my understanding of how this all fits together. I'd really appreciate if someone could help me understand this, or point me towards a good source of info. I've been through the info on github and the related Railscast, but I'm still missing something.
It is impossible to search associated records with multisearch, due to how polymorphic associations work in Rails and SQL.
I will add an error that explains the situation so that in the future it won't be as confusing.
Sorry for the confusion.
What you could do instead is define a method on Car that returns the text you wish to search against.
class Car < ActiveRecord::Base
include PgSearch
multisearchable :against => [:name, manufacturer_name]
belongs_to :manufacturer
def manufacturer_name
manufacturer.name
end
end
Or to be even more succinct, you could delegate:
class Car < ActiveRecord::Base
include PgSearch
multisearchable :against => [:name, manufacturer_name]
belongs_to :manufacturer
delegate :name, :to => :manufacturer, :prefix => true
end
But you have to make sure the pg_search_documents table gets updated if you ever make a name change to a Manufacturer instance, so you should add :touch => true to its association:
class Manufacturer < ActiveRecord::Base
has_many :cars, :touch => true
end
This way it will call the Active Record callbacks on all the Car records when the Manufacturer is updated, which will trigger the pg_search callback to update the searchable text stored in the corresponding pg_search_documents entry.

How can one mongoid model query another?

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.

Polymorphic Assocations using Integer ID type fields

I have a table Foo that has a polymorphic belongs_to association called bar. The foos table has the standard bar_id column. However, instead of a string-based bar_type column, I have an integer bar_type_id column. This column references the id column in the table bar_types. bar_types.name holds the name of the class that represents the class of the particular bar instance.
Does Rails (ideally >=2.3.10) allow for this type of polymorphic association?
We did it by overriding the association_class method in a new module and included it using the :extend option. Also created a integer to string mapping hash to make things easier.
In config/initializers directory or anywhere you like, create a file and define the hash
INT_OBJECT_TYPE_TO_CLASSNAME = { 0 => "Project", 1 => "Task", 2 => "Timesheet" }
class CommentObjectType < ActiveRecord::Base
module ClassNamesAsInt
def association_class
return INT_OBJECT_TYPE_TO_CLASSNAME[restricted_object_type].constantize
end
end
end
In comments.rb
belongs_to :commentable, :polymorphic => true, :extend => CommentObjectType::ClassNamesAsInt
I'm making use of the polymorphic integer type gem, written by one of my co-workers. It's slightly easier to use than the examples given above, in my opinion. For example, after configuring the mapping, you change from:
belongs_to :actor, polymorphic: true
to the new format:
belongs_to :actor, polymorphic: true, integer_type: true
There are two approaches for this.
First is easy:
has_many :bars, :conditions => "whatever you want"
Second could be tricky:
set_inheritance_column :bar_type_id
I am not sure, but you can play around
belongs_to :bar, :class_name => proc{ BarType.find(self.bar_type_id).name }, :foreign_key => :bar_id

Resources