Let's say I have an Order model, that contains many products. I want to be able to keep track of which products are shipped, and which aren't, so I would like to keep track of some metadata that goes along with each relation. If this was a has_one relation, then it would be simple, just insert a few more fields.
How can I accomplish this with a has_many relation between an Order model and a Product model cleanly using Mongoid?
I solved this myself my using another model OrderProduct as a proxy between the two. The implementation of the state was done using the state_machine gem. I ended up wrapping a lot of the calls to the product's state machine in the Order model. This allows me to call things like .product_state(product) or can_cancel_product?(product) on the order instance.
Order
class Order
include Mongoid::Document
include Mongoid::Timestamps
field :state, type: String
embeds_many :order_products
state_machine initial: :open do
...
end
def products
order_products.map do |op|
op.product
end.freeze
end
def add_product(product)
OrderProduct.create({_product: product._id, order: self})
end
def remove_product(product)
order_products.delete find_order_product(p)
end
#wrapper for product events
OrderProduct.new.state_paths.events.each do |event|
define_method "#{event}_product" do |product|
find_order_product(product).send(event)
end
define_method "can_#{event}_product?" do |product|
find_order_product(product).send("can_#{event}?")
end
end
#wrapper for product states
OrderProduct.state_machine.states.map(&:name).each do |state|
define_method "product_#{state}?" do |product|
find_order_product(product).send("#{state}?")
end
end
#wrapper for product current state
def product_state(product)
find_order_product(product).state
end
private
def find_order_product(p)
order_products.at(order_products.index do |op|
op.product == p
end)
end
end
OrderProduct
class OrderProduct
include Mongoid::Document
embedded_in :order
field :_product, type: Moped::BSON::ObjectId
state_machine initial: :open do
....
end
def product
Product.find(_product)
end
def product=(product)
_product = (product._id)
end
end
Related
I need to create 4 records in table inventory after creating a user. I know that i need to use callback after_create, but i guess it is not good according to the best practices and DRY principle to have 4 lines like this:
def create_items
Item.create({user_id: self.id, item_id: 1})
Item.create({user_id: self.id, item_id: 2})
...
end
Or maybe even like that?
def create_items
self.inventories.create([
{item_id: 1},
{item_id: 2}
])
end
Ofen you can solve this with has_many :through:
class Inventory
belongs_to :user
belongs_to :item
end
class User
has_many :inventories
has_many :items, through: :inventories
end
Then you can add them straight-up:
items.each do |item|
#user.items << item
end
If you have only ID values:
Item.where(id: ids).each do |item|
#user.items << item
end
That's generally efficient enough to get the job done. If you're experiencing severe load problems with this you can always do it with a bulk-insert plugin, but that's usually a last-resort.
There have 2 tables: Orders and Arrivals. There can be many arrivals on an order. I want to validate the creation of arrivals for a specific order.
Orders has fields book_id and quantity:integer
Arrivals has fields order:belongs_to and quantity:integer
Order.rb:
class Order < ActiveRecord::Base
has_many :arrivals
def total_arrival_quantity
arrivals.map(&:quantity).sum
end
def order_quantity_minus_arrival_quantity
quantity - total_arrival_quantity
end
end
Arrival.rb:
class Arrival < ActiveRecord::Base
belongs_to :order
validates :total_arrival_quantity_less_or_equal_to_order_quantity, on: create
validates :current_arrival_quantity_less_or_equal_to_order_quantity, on: create
def current_arrival_quantity_less_or_equal_to_order_quantity
self.quantity <= order.quantity
end
end
How can I make the two validations work?
Something like this should work,
validate :order_quantity, on: :create
private
def order_quantity
if quantity > order.order_quantity_minus_arrival_quantity
errors.add(:quantity, 'cannot be greater than ordered quantity.')
end
end
I am trying to set search on a model that has a lot of different associations. I am starting with the belongs_to associations. I am able to search on the name field of the Product model successfully but the when I perform a search on what would be in the associated models I just get the default results.
What am I doing wrong?
Any help would be much appreciated.
#Product Model
Class Product < ActiveRecord::Base
searchable do
text :name
integer :store_id, :references => Store.name
text :store do
Store.all.map { |store| store.name }
end
end
end
#product controler
def search
#search = Sunspot.search(Product) do
fulltext params[:search] do
fields(:name, :store)
end
end
#products = #search.results
end
#Store Model
searchable do
text :name
end
Class Product < ActiveRecord::Base
belongs_to :store
searchable do
text :name
index :store do
index :name
end
integer :store_id # do you really need this? I think not.
end
end
Don't forget to reindex after each change in your models.
EDIT: You don't need to index the Store class by itself, unless you plan to search on it.
Let's assume I have a grandparent document with many parents, and each parent has many children.
What is the best way, in Rails with Mongoid, to get all of the children for a specific grandparent without looping?
For example, if I were to use loops, it would look something like this (rough code):
def children
children = []
parents.each do |p|
p.children.each do |c|
children << c
end
end
children.uniq
end
class Grandparent
include Mongoid::Document
has_many :parents
end
class Parent
include Mongoid::Document
belongs_to :grandparent
has_many :children
end
class Child
include Mongoid::Document
belongs_to :parent
end
A method like this, it would load the children as an attribute once called.
def children(reload=false)
#children = nil if reload
#children ||= Child.where(:parent_id.in => parents.map(&:id))
end
See this SO answer as well
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