Mongoid presence_of embedded document and destroy embedded - ruby-on-rails

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!

Related

Validates association in child class of a model

I am facing strange issue in validating presence of association in a derived class.
Class Contact < ActiveRecord::Base
has_and_belongs_to_many :things
end
Class SubContact < Contact
# validates_presence_of :things
validate :must_have_things
def must_have_things
if things.blank?
errors.add("Must select things")
end
end
end
In SubContact class, I am trying to validate presence of things but nothing is working. I have tried both custom and built in validators
How can I achieve this?
add needs an attribute. Try using:
errors.add(:base, "Must not be blank")
In this case, it is the base class but it can be any other attribute too. Mention an attribute name of the SubContract class or :base.

Unusual Rails Mongoid case

Previously we had has_one and belongs_to relation with our models:
class Task
include Mongoid::Document
include Mongoid::Timestamps
has_one :output
end
class Output
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :task
end
But we now plan to embed output inside task.
class Task
include Mongoid::Document
include Mongoid::Timestamps
embeds_one :output
end
class Output
include Mongoid::Document
include Mongoid::Timestamps
embedded_in :task
end
Everything works fine but we want to make backward compatible. ie. we want those output also which were created before embed.
Then, we did this method in task.rb:
def output
Task.collection.find(_id: Moped::BSON::ObjectId(self.id)).first.output || Output.collection.find(task_id: Moped::BSON::ObjectId(self.id)).first
end
The problem with this is now task.output will give json instead of output object.
so we cannot do
task = Task.new
output = task.create_output
output.task #=> not possible
Anyone having this scenario or any directions for this case.
Well, instead of making a workaround, why dont we migrate the old ones?
First, change both models to embed by replacing has_one with embeds_one and replacing belongs_to with embedded_in. Save the code.
Then use your rails console (>> rails console)
Then
Output.each do |o|
if !o.task_id.nil?
#change to embedded format
t=Task.find(o.task_id)
t.output=o
t.output.task_id=nil
t.save
end
end

How to remove Rails nested association before running validations?

I'm using Mongoid and embedded documents. I am using the standard approach to using a hidden form field of _destroy with a value of 1. This works fine, except for when a validator is run which includes the association that I'm trying to delete. For example:
class Thing
include Mongoid::Document
embeds_many :actions
validate :uniqueness_of_actions
def uniqueness_of_actions
subjects = actions.map(&:subject)
subjects_are_unique = subjects == subjects.uniq
errors.add(:actions, 'must have unique subjects') unless subjects_are_unique
subjects_are_unique
end
end
What is the proper way to remove/exclude the associations marked for destruction before validations are run which might include them?
Try something along these lines.
class Artist < AR::Base
has_many :songs
validate :custom_thing
def custom_thing
songs.reject{ |x| x._destroy}.each do |a|
# magic here
end
end
end

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

Mongoid: Limiting number of embedded objects

I am using Mongoid with Rails 3. What would be the best way to limit the number of embedded objects (photos) that can be stored within each parent object (album)?
class Album
include Mongoid::Document
embeds_many :photos
end
class Photo
include Mongoid::Document
embedded_in :album, :inverse_of => :photos
end
With ActiveRecord, I would do something like:
has_many :photos, :before_add => :enforce_photo_limit
private
def enforce_photo_limit
raise "Too many photos" if self.photos.count >= 50
end
...but this isn't supported by Mongoid.
Any suggestions much appreciated.
Thanks.
Mongoid includes ActiveModel::Validations, so you should be able to use the methods contained in that module:
class Album
include Mongoid::Document
embeds_many :photos
validate :less_than_fifty_photos
def less_than_fifty_photos
errors.add(:base, "Too many photos") if self.photos.count >= 50
end
end
More info: http://mongoid.org/docs/validation.html
You can also use validates_length_of and it should work.

Resources