How to migrate from belongs_to, to embedded_in in Mongoid? - ruby-on-rails

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

Related

Can I add both sides of a has_many and belongs_to relationship in a concern?

Edit: In retrospect this isn't that great of an idea. You are putting functionality that belongs to ZipWithCBSA into the models of others. The models receiving the concern act as they are supposed to, and the fact that ZipWithCBSA responds to :store_locations should be obvious in some capacity from ZipWithCBSA. It has nothing to do with the other models/concern. Props to Robert Nubel for making this obvious with his potential solutions.
Is it possible to both has_many and belongs_to relationships in a single concern?
Overview
I have a table ZipWithCBSA that essentially includes a bunch of zip code meta information.
I have two models that have zip codes: StoreLocation and PriceSheetLocation. Essentially:
class ZipWithCBSA < ActiveRecord::Base
self.primary_key = :zip
has_many :store_locations, foreign_key: :zip
has_many :price_sheet_locations, foreign_key: :zip
end
class StoreLocation< ActiveRecord::Base
belongs_to :zip_with_CBSA, foreign_key: :zip
...
end
class PriceSheetLocation < ActiveRecord::Base
belongs_to :zip_with_CBSA, foreign_key: :zip
...
end
There are two properties from ZipWithCBSA that I always want returned with my other models, including on #index pages. To prevent joining this table for each item every time I query it, I want to cache two of these fields into the models themselves -- i.e.
# quick schema overview~~~
ZipWithCBSA
- zip
- cbsa_name
- cbsa_state
- some_other_stuff
- that_I_usually_dont_want
- but_still_need_access_to_occasionally
PriceSheetLocation
- store
- zip
- cbsa_name
- cbsa_state
StoreLocation
- zip
- generic_typical
- location_address_stuff
- cbsa_name
- cbsa_state
So, I've added
after_save :store_cbsa_data_locally, if: ->(obj){ obj.zip_changed? }
private
def store_cbsa_data_locally
if zip_with_cbsa.present?
update_column(:cbsa_name, zip_with_cbsa.cbsa_name)
update_column(:cbsa_state, zip_with_cbsa.cbsa_state)
else
update_column(:cbsa_name, nil)
update_column(:cbsa_state, nil)
end
end
I'm looking to move these into concerns, so I've done:
# models/concerns/UsesCBSA.rb
module UsesCBSA
extend ActiveSupport::Concern
included do
belongs_to :zip_with_cbsa, foreign_key: 'zip'
after_save :store_cbsa_data_locally, if: ->(obj){ obj.zip_changed? }
def store_cbsa_data_locally
if zip_with_cbsa.present?
update_column(:cbsa_name, zip_with_cbsa.cbsa_name)
update_column(:cbsa_state, zip_with_cbsa.cbsa_state)
else
update_column(:cbsa_name, nil)
update_column(:cbsa_state, nil)
end
end
private :store_cbsa_data_locally
end
end
# ~~models~~
class StoreLocation < ActiveRecord::Base
include UsesCBSA
end
class PriceSheetLocation < ActiveRecord::Base
include UsesCBSA
end
This is all working great-- but I still need to manually add the has_many relationships to the ZipWithCBSA model:
class ZipWithCBSA < ActiveRecord::Base
has_many :store_locations, foreign_key: zip
has_many :price_sheet_locations, foreign_key: zip
end
Finally! The Question!
Is it possible to re-open ZipWithCBSA and add the has_many relationships from the concern? Or in any other more-automatic way that allows me to specify one single time that these particular series of models are bffs?
I tried
# models/concerns/uses_cbsa.rb
...
def ZipWithCBSA
has_many self.name.underscore.to_sym, foregin_key: :zip
end
...
and also
# models/concerns/uses_cbsa.rb
...
ZipWithCBSA.has_many self.name.underscore.to_sym, foregin_key: :zip
...
but neither worked. I'm guessing it has to do with the fact that those relationships aren't added during the models own initialization... but is it possible to define the relationship of a model in a separate file?
I'm not very fluent in metaprogramming with Ruby yet.
Your best bet is probably to add the relation onto your ZipWithCBSA model at the time your concern is included into the related models, using class_exec on the model class itself. E.g., inside the included block of your concern, add:
relation_name = self.table_name
ZipWithCBSA.class_exec do
has_many relation_name, foreign_key: :zip
end

Using sunspot to search down model hierarchy

Example:
I have the following:
class Person < ActiveRecord::Base
has_many :educations
end
class Education < ActiveRecord::Base
belongs_to :school
belongs_to :degree
belongs_to :major
end
class School < ActiveRecord::Base
has_many :educations
# has a :name
end
I want to be able to return all people who went to a specific school so in my PeopleController#index I have
#search = Person.search do
keywords params[:query]
end
#people = #search.results
How do I create the searchable method on the Person model to reach down into school? Do I do something like this:
searchable do
text :school_names do
educations.map { |e| e.school.name }
end
end
which I would eventually have to do with each attribute on education (degree etc) or can I make a searchable method on Education and somehow "call" that from Person.searchable?
Thanks
It would be best if you keep the declaration of all the indexed fields for an specific model in the same place.
Also, you were doing a good job indexing :school_names, just do the same thing for the rest of the associations fields' that you want to index.

Mongoid presence_of embedded document and destroy embedded

I have some mongoid document:
class Firm
include Mongoid::Document
embeds_many :offices
validates_presence_of :offices
end
At least one office must be present. It works.
However when 'destroy' method called for latest office than firm was saved but not valid anymore..
I can use something like this:
class Office
embedded_in :firm
before_destroy :check_for_latest
def check_for_latest
false if firm.offices.count == 1
end
end
but it's not good way
Any ideas? Thanks!

Mongoid Circular References

So I have a circular reference problem that I don't know how to solve. So the situation is the user has the ability to input a list of multiple films for actors into a text box. The text is held in a virtual attribute and parsed and given to the models in a model's save callback. Ideally I would not like to change the structure of the models because it makes english sense and seems very normal from a database standpoint.
Here's the structure:
require 'mongoid'
require 'mongo'
class Actor
include Mongoid::Document
field :name
attr_accessible :user_input_films
attr_accessor :user_input_films
before_save :assign_films
embeds_one :filmography
belongs_to :cast
def assign_films
if user_input_films == nil
return
end
user_input_films.split(" ").each do |film|
film = Film.first(conditions: { :name => film})
if film == nil
film = Film.new(:name => film)
film.build_cast
film.save!
end
self.filmography.add_film(film)
end
end
end
class Filmography
include Mongoid::Document
has_many :films
embedded_in :actor
def add_film(film)
films << film
film.cast.actors << self.actor
end
end
class Film
include Mongoid::Document
field :name
embeds_one :cast
belongs_to :filmography
end
class Cast
include Mongoid::Document
embedded_in :film
has_many :actors
end
Mongoid.configure do |config|
config.master = Mongo::Connection.new.db("mydb")
end
connection = Mongo::Connection.new
connection.drop_database("mydb")
database = connection.db("mydb")
actor = Actor.new
actor.build_filmography
actor.user_input_films = "BadFilm1 BadFilm2"
actor.save
puts "actor = #{actor.attributes}"
actor.filmography.films.each do |film|
puts "film = #{film.attributes}"
end
So the structure is: Actor owns Filmography which references many Films. And Film owns Cast which references many Actors. And the problem happens in the line:
film.cast.actors << self.actor
because the actor is then saved again through the << operator and the circular logic happens again
And as everyone guessed the error is:
/home/greg/.rvm/gems/ruby-1.9.2-p290#rails31/gems/mongoid2.2.1/lib/mongoid/fields.rb:307: stack level too deep (SystemStackError)
So how can I save my document without the circular reference stack overflow?
UPDATE:
One Thought, I think a dirty solution would be a way of adding a reference without saving the referenced object.
Thanks

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.

Resources