Changing Mongoid class name mid-production - ruby-on-rails

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 !

Related

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

Finding a specific model with some condition associated with another model

I've checked like every other question here and can't seem to find the answer.
I have
Path:
attr_accessible :location, :total_time, :visits
belongs_to :trackedsite, :touch => true
Trackedsite:
attr_accessible :total_time, :url, :visits
has_many :paths, :autosave => true
I want to find a Path belonging to a Trackedsite with a specific location
I have tried:
#trackedsite = Trackedsite.find_by_url(params[:url_string])
#path = #trackedsite.find_by_location(params[:path_string])
But I get "no such column: paths.trackedsite_id" (which is correct, that column doesn't exist, should I be adding something to my migration?)
Associations work by having foreign keys. If your path belongs_to a TrackedSite by convention there needs to be a trackedsite_id, otherwise there's no link between the entities.

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.

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

Rails AR validates_uniqueness_of against polymorphic relationship

Is it posible to validate the uniqueness of a child model's attribute scoped against a polymorphic relationship?
For example I have a model called field that belongs to fieldable:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => :fieldable_id
end
I have several other models (Pages, Items) which have many Fields. So what I want is to validate the uniqueness of the field name against the parent model, but the problem is that occasionally a Page and an Item share the same ID number, causing the validations to fail when they shouldn't.
Am I just doing this wrong or is there a better way to do this?
Just widen the scope to include the fieldable type:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => [:fieldable_id, :fieldable_type]
end
You can also add a message to override the default message, or use scope to add the validation:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :fieldable_id, :scope => [:fieldable_id, :fieldable_type], :message => 'cannot be duplicated'
end
As a bonus if you go to your en.yml, and enter:
activerecord:
attributes:
field:
fieldable_id: 'Field'
You are going to replace the default 'subject' that rails add to the errors with the one you specify here. So instead of saying: Fieldable Id has been already taken or so, it would say:
Field cannot be duplicated

Resources