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.
Related
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
So I have a few different models in my Rails 4 app that have image uploads. Rather than adding identical code to each of the models I've created a module that I can include into all of them.
Here it is:
module WithImage
extend ActiveSupport::Concern
included do
attr_accessor :photo
has_one :medium, as: :imageable
after_save :find_or_create_medium, if: :photo?
def photo?
self.photo.present?
end
def find_or_create_medium
medium = Medium.find_or_initialize_by_imageable_id_and_imageable_type(self.id, self.class.to_s)
medium.attachment = photo
medium.save
end
end
def photo_url
medium.attachment if medium.present?
end
end
class ActiveRecord::Base
include WithImage
end
A Medium (singular of media) in this case is a polymorphic model that has paperclip on it. The attr_accessor is a f.file_field :photo that I have on the various forms.
Here's my PurchaseType Model (that uses this mixin):
class PurchaseType < ActiveRecord::Base
include WithImage
validates_presence_of :name, :type, :price
end
So here's the thing, the after_save works great here. However, when I go to the console and do PurchaseType.last.photo_url I get the following error:
ActiveRecord::ActiveRecordError: ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
I haven't the faintest clue what this means or why it is happening. Anyone have any insight?
Thanks!
It turns out I was trying to do things I had seen in various examples of modules. It was simple to get it working:
module WithImage
extend ActiveSupport::Concern
included do
attr_accessor :photo
has_one :medium, as: :imageable
after_save :find_or_create_medium, if: :photo?
def photo?
self.photo.present?
end
def find_or_create_medium
medium = Medium.find_or_initialize_by_imageable_id_and_imageable_type(self.id, self.class.to_s)
medium.attachment = photo
medium.save
end
def photo_url
medium.attachment.url if medium.present?
end
end
end
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
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!
I'd like to use the countries gem I found here instead creating a separate model.
It works fine to inherit from but I'd also like to be able to have other classes belong_to it.
Is this possible? IE something like below. Is there some method I could use to provide a key to child classes?
https://github.com/hexorx/countries
class Country < ISO3166::Country
#include Mongoid::Document
#RELATIONS
has_many :cities
has_many :reviews, as: :reviewable
end
At the moment I get NoMethodError: undefined method `has_many' for Country:Class
Or some way to include/inherit the attributes from the gem after the object is initialized?
class Country# < ISO3166::Country
include Mongoid::Document
#field :name, :type => String
field :country_id, :type => String
##RELATIONS
has_many :cities
has_many :reviews, as: :reviewable
def after_initialize
ISO3166::Country.find_country_by_alpha3(self.country_id)
end
end
To me the best behavior is not using has_many in your case but create method you want with Mongoid query inside.