Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Mongoid, how do I create an array or references with quantities

I have a

class Item
  include Mongoid::Document
  field name, type: String
end

and a

class Pack
  include Mongoid::Document
end

Each Pack can include different Items in different quantities.

I initially wanted to do

class Pack
  include Mongoid::Document

  field items, type: Array
end

At first, I tried

a = Pack.new
a.items = []
a.items << {item: Item.first, quantity: 4}
a.save

and I got:

NoMethodError: undefined method `__bson_dump__' for #<Item:0x007faf1a56d670>

Then I tried:

a = Pack.new
a.items = []
a.items << {item_id: Item.first.id, quantity: 4}
a.save

But now I can't do something like

a.items[0].item.name

Next, I tried

class Item
  include Mongoid::Document
  field name, type: String
  belongs_to :item_quantity
end

class ItemQuantity
  include Mongoid::Document
  has_one :item
  belongs_to :pack
  field quantity, type: Integer
end

class Pack
  include Mongoid::Document
  has_many :item_quantity
end

Which works but feels like a kludge

How do I best do this?

like image 546
Shalmanese Avatar asked Nov 16 '25 17:11

Shalmanese


1 Answers

Your last iteration is close to the way I've solved similar problems in the past. I'll try to adapt it to your context. The main thing I did differently was not storing the quantity in the "join" collection, so for each of the multiple "copies" of an item in a pack, a new doc gets created in that collection.

class Item
  include Mongoid::Document
  has_many :item_instances
end

class ItemInstance
  include Mongoid::Document
  belongs_to :item
  belongs_to :pack
end

class Pack
  include Mongoid::Document
  has_many :item_instances      
end

With this you can do something like:

pack = Pack.new
item = Item.first
pack.item_instances.build(item_id: item.id)

I've used this for a print shop management app where the classes were Invoice, Job, Product. A job was sort of an "instance" of a product, and an invoice had many jobs, which could possibly all be the same product, so a straight many to many mapping wouldn't work.

I'd also rethink your schema and make sure things are named meaningfully. Just by renaming your classes from Item, ItemInstance, Pack to Product, Item, Pack respectively, it makes things a bit clearer:

pack = Pack.new
product = Product.first
pack.items.build(product_id: product.id)

Finally, if you create some simple convenience methods in the Pack class, that can go a long way towards creating a more useable interface:

class Pack
  def new_item(product)
    self.items.build(product_id: product.id)
  end

  def products
    self.items.map { |i| i.product }
  end
end

So you can do:

pack.add_item(product)
# Same as using pack.items.build(product_id: product.id)

pack.products
# Returns an array (including duplicates) of all the products included in the pack.

Hope that helps. This doesn't come up often, and I'm sure there are other solutions, but that's the basic pattern I use when trying to solve the "one object has more than one of another object" problem.

Add your own methods relevant to how you'll be using the class and it should be pretty usable. Also, don't forget to set up indexing on the "join" collection. And see this issue on Mongoid's Github page, requesting multiple entries in the arrays used for many-to-many relations.

If that gets added, it'll be the way to go in the future if all you need is a simple relation. If you to store more details, like I did when a print job for a product gets created (timestamp, title etc.), you'll still have to do something like the above.

like image 145
Vickash Avatar answered Nov 19 '25 08:11

Vickash