I am writing a simple accounting system for managing costs. The structure is like this:
Invoice - can have many products
Product - can have many costs, also can act_as_tree
LineItem -
Product
LineItem
LineItem
Product
LineItem
Product
LineItem
LineItem
I had this set up as a has_many and belongs_to for the three classes but think that the following is more appropriate (based upon reading Rails 3 Way - any shortcomings are my lack of understanding; just trying to give context)
Class Invoice < ActiveRecord::Base
has_many :products
has_many :line_items, :through => :products
end
Class Product < ActiveRecord::Base
belongs_to :invoice
belongs_to :line_item
end
class LineItem < ActiveRecord::Base
has_many :products
has_many :invoices, :through => :invoices
end
But I don't this is working correctly.
if I do the following:
>#i=Invoice.find(1)
>#i.products # this works
>#i.line_items # doesn't work, unidentified method line_items
This is the first time I'm using has_many :through. Is this set up correctly for my data model? Also, is it possible to use it in conjunction with acts_as_tree - I'd like to be able to say:
>#i.line_items
and get back all the line items for that specific invoice. Possible?
thx for help
First a question: What is your relation between Product and LineItem: Has 1 product many line items or is 1 and the same line item referenced in many products? The rest of this answer is based on the assumption that every product should have multiple line items.
I think your models should be defined like that:
# No change
Class Invoice < ActiveRecord::Base
has_many :products
has_many :line_items, :through => :products
end
# Changed the relation to :line_items
Class Product < ActiveRecord::Base
belongs_to :invoice
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :products
# The following then does not make sense
##has_many :invoices, :through => :invoices
end
why did you choose this structure? in such cases i usually do
Class Invoice < ActiveRecord::Base
has_many :line_items
has_many :products, :through => :line_items
end
Class Product < ActiveRecord::Base
has_many :line_items
# if you need
has_many :products, :through => :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :products
belongs_to :invoice
end
this fullfills following requirements:
Invoice and Product have a Many2Many relationship
The relationship between an Invoice and a Product is a LineItem, which provides further information like price, amount, applied taxes, etc.
You can create a LineItem by adding a Product:
invoice = Invoice.create
invoice.products << Product.find_by_name('awesome')
Your Invoice should only have line items! These can be a tree structure but you should not have products directly referenced from your invoice (what if a product price changes: this would affect existing invoices!)
So, to fix your structure:
invoice
line_item # references a product, but has its own fields (price!)
line_item
line_item
Each line_item should reference one product using belong_to. It's your join model between invoices and products:
class Invoice
has_many :line_items
end
class LineItem
belongs_to :invoice
belongs_to :product
acts_as_tree # (implies has_many :line_items with the parent invoice_id, etc.)
end
class Product
has_many :line_items
end
This is basically all you need to build an invoice. You can think of the tree structure as being independent from the Invoice:LineItem:Product relationship. It can work as a flat list, or enhanced to become a tree.
If your products normally contain other products and you need to know which children to add to the invoice when the parent product is added (as line items), you'll need to tree your products too:
class Product
has_many :line_items
acts_as_tree
end
This tree structure is independent of the tree structure in line items. Remember: products can change, and this shouldn't affect existing line items in your invoices.
Related
I am trying to wrap my head around how I am going to be able to have a has_many (or has_one in my case) association with a compound foreign_key.
For my example Assume this:
Imagine I am managing relationships between sellers and buyers, there is an initial contract between them and after that any amount of invoices. So the models look like this:
class Invoice
belongs_to :seller
belongs_to :buyer
end
class Seller
has_many :contracts
has_many :invoices
end
class Buyer
has_many :contracts
has_many :invoices
end
class Contract
belongs_to :seller
belongs_to :buyer
end
For every invoice there is one initial contract (defined by the seller and the buyer). I'm aware that there is all kinds of solutions to build a query that achieves that. Ideally though I want to take care of preloading/joining the contract for a list of invoices to avoid N+1 problems:
class Invoice
belongs_to :seller
belongs_to :buyer
has_one :contract # options go here
end
EDIT: The schema is what it is. Solutions that require a schema change unfortunately is not an option.
Just make the invoice belong to the contract and get the buyer and seller from that.
class Invoice
belongs_to :contract
end
Then get the invoices through contacts...
class Buyer
has_many :contracts
has_many :invoices, through: :contracts
end
class Seller
has_many :contracts
has_many :invoices, through: :contracts
end
I'm having trouble figuring out the proper way of retrieving all children of multiple parents through association chaining.
To simplify I have three models:
class Customer < ActiveRecord::Base
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :invoice
end
After creating a few objects I tired to use the example from rails guides (association basics: 4.3.3.4 includes):
Customer.first.invoices.line_items
It returns:
undefined method `line_items' for #<Customer::ActiveRecord_Associations_CollectionProxy
Is grandparent.parents.children not usable?
EDIT
I'm not searching for the grandparent.parents.first.children, but all children of all parents in the collection, rails guides state:
If you frequently retrieve line items directly from customers (#customer.orders.line_items),
As a valid operation, I would like to know if that is a mistake.
FINAL As stated in the comments of the selected answer: in ActiveRecord: scopes are chainable but associations are not.
The customer.invoices.line_items cannot work the way you want to, since the has_many always is linked to a single record. but you can achieve what you want (if I understand correctly) using has_many through
as follows:
class Customer < ActiveRecord::Base
has_many :invoices
has_many :line_items, through: :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :invoice
end
and now you can write:
customer.line_items
and it will return all line_items which are connected to a customer's invoices.
Customer.first.invoices.first.line_items
Or if you want all of the data together, you can do something like:
results = Customer.first.invoices.includes(:line_items)
Then you may access data with no DB call, by looping results. For first data ex: results.first.line_items
Hope it helps!
Customer.first.invoices will return an collection (like an array) of invoices. The line_items method isn't defined for a collection, but its defined for an invoice. Try Customer.first.invoices.first.line_items
EDIT - If you always want the orders to include the line items, you can just do:
class Customer < ActiveRecord::Base
has_many :orders, -> { includes :line_items }
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end
I have two models, User and Product
class User < ActiveRecord::Base
has_and_belongs_to_many :products
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :users
end
Now I want to make three lists for each user, products they've checked out recently, products that are in their shopping cart and products which they have bought. How do I make a distinction between these relations? Could I add some sort of type column to the relation table? And how could I then later check this type?
Thanks
If you and to add other columns to the relation table perhaps you should consider using has_many on both User and Products, then you can add your columns on Cart
class User < ActiveRecord::Base
has_many :carts
has_many :products, through: :carts
end
class Cart < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
class Product < ActiveRecord::Base
has_many :carts
has_many :users, through: :carts
end
I have this relationship between categories, products & brands:
class Brand < ActiveRecord::Base
has_many :products
end
class Category < ActiveRecord::Base
has_and_belongs_to_many :products
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :categories
belongs_to :brand
end
How can I select all categories by specified brand with this relations?
I try this but get an error
b = Brand.find(1)
Category.joins(:products).where(:products => b.products)
You did the right thing with the join, just add a more complex where definition:
Category.joins(:products).where(:products => {:brand_id => 1})
Controversially HABTM's are rarely, if ever, a good design and IMO just about the only thing Rails got wrong.
Introduce an xref table to join products and categories and use has_many :through on both sides of the relationship so you end up with
class Brand < ActiveRecord::Base
has_many :products
has_many categories :through => products # This is now allowed in Rails 3.x and above
end
class Category < ActiveRecord::Base
belongs_to :product_category
has_many :products :through => product_category
end
class Product < ActiveRecord::Base
belongs_to :brand
belongs_to :product_category
has_many :categories :through => product_category
end
class ProductCategory < ActiveRecord::Base
has_many :products
has_many :categories
end
This gives you the best flexibility with the least amount of code re-factoring for you plus a much more intuitive path to get whatever data you need on either side of the relationship and will enable you to achieve the following
b = Brand.find(1)
b.categories.all
Update
The above is totally untested code and I have just corrected a glaringly stupid mistake I made. If you have any issues implementing this then come back
I'm having a hard time setting up the basic structure of my databases.
I have products (about 50). Each products is related to one or more place(s).
The basic schema would be this (no relation yet)
Products
id:integer
name:string
Places
id:integer
name:string
content:string
I first tought I could connect places and products by adding place_id to products and has_many belong_to in the controllers, but since a product can have more than one place, I don't know how.
If a Product has_many Places and a Place has_many Products your association needs to be many-to-many.
There are two ways to do this in Rails, the most recommended is a join model. You explicitly label the relationship between Products and Places. You could call it ProductLocation or Shop or similar, it would look like this:
product_locations:
product_id
place_id
class ProductLocation < ActiveRecord::Base
belongs_to :product
belongs_to :place
end
class Place < ActiveRecord::Base
has_many :product_locations
has_many :products, :through => :product_locations
end
class Product < ActiveRecord::Base
has_many :product_locations
has_many :places, :through => :product_locations
end
See http://guides.rubyonrails.org/association_basics.html#choosing-between-has_many-through-and-has_and_belongs_to_many
Just use through connection
ProductsPlaces (new table)
product_id
place_id
And in models
class Product < ActiveRecord::Base
has_many :places, :through => :products_places
end
class Place < ActiveRecord::Base
has_many :products, :through => :products_places
end
Also there is has_and_belongs_to_many which do in fact the same (with ProductsPlaces table)
class Product < ActiveRecord::Base
has_and_belongs_to_many :places
end
class Place < ActiveRecord::Base
has_and_belongs_to_many :products
end
But better use through, because has_and_belongs_to_many will be deprecated.
It sounds like you want to add product_id to places.
Product has_many Places
Place belongs_to Product