has_many :through best practices.

I don't find useful examples on using has_many through associations. First of all I need to know if it is necessary to have a model. I have this type of association: models: User, Bag. User has_many :deliveries, has_many :bags, :through => :deliveries, Bag has_many :deliveries, has_many :ruser, :through => :deliveries. I need to create Delivery model?

Yes, Delivery belongs_to user and belongs_to bag. But if there is no other data in Delivery then you could use User has_and_belongs_to_many :bags Bag has_and_belongs_to_many :users then you don't need deliveries, but you still need to provide a table bags_users containing the ids. I tend to favour having a model though, and using :through as you have suggested, especially if the join table has some meaning in the real world (such as deliveries).

Have you seen the Rails Guide on ActiveRecord Associations, I think it should have examples of both of these strategies.

Colin

In Delivery I have the attribute delivered_at of type date. This is why I used :through => deliveries, I think this is correct. Isn't it?

Yes, that is fine. In that case you obviously need a Delivery model and a deliveries table, otherwise you will have no way of setting the date.

Colin

Ok, this now it's clear. But how can I assign bags with delivered_at to users? I've created users and bags, then I've tried: user=User.first, bag=Bag.first Delivery.create(:user_id=>user.id, :bag_id=>bag.id: delivered_at=>'2011-01-01') but when I do user.deliveries I have no data and also for user.bags.

There are much better ways of doing that, but that should work. Can you post the class definitions for your three classes, and also db/schema.rb?

Colin

Yes:

class Ruser < ActiveRecord::Base   has_many :deliveries   has_many :bags, :through => :deliveries   has_one :document   default_scope :order => 'fullname ASC'

  def self.search(fullname)     where('fullname ILIKE ?', "#{fullname}%") if fullname   end end

class Bag < ActiveRecord::Base   has_many :deliveries   has_many :rusers, :through => :deliveries end

class Delivery < ActiveRecord::Base   belongs_to :ruser   belongs_to :bag end

ActiveRecord::Schema.define(:version => 20110114084202) do

  create_table "bags", :force => true do |t|     t.string "bag_type"     t.datetime "created_at"     t.datetime "updated_at"   end

create_table "delegates", :force => true do |t|     t.string "fullname"     t.datetime "created_at"     t.datetime "updated_at"   end

  create_table "deliveries", :force => true do |t|     t.integer "ruser_id", :limit => 10     t.integer "bag_id", :limit => 10     t.date "delivery_at"     t.datetime "created_at"     t.datetime "updated_at"   end

  create_table "documents", :force => true do |t|     t.string "doctype"     t.string "docnumber"     t.integer "ruser_id", :limit => 10     t.integer "delegate_id", :limit => 10     t.datetime "created_at"     t.datetime "updated_at"

end

  create_table "rusers", :force => true do |t|     t.integer "year", :limit => 10     t.integer "code", :limit => 10     t.string "fullname"     t.date "birthdate"     t.string "homeaddress"     t.string "homepostalcode", :limit => 6     t.string "homelocality"     t.string "fiscalcode", :limit => 16     t.string "contractnumber"     t.integer "squaremeters", :limit => 10     t.integer "category", :limit => 10     t.string "taxedaddress"     t.datetime "created_at"     t.datetime "updated_at"   end

end

I've not found any exhaustive examples of has_many through. Which are the better ways?

Yes, that is fine. In that case you obviously need a Delivery model and a deliveries table, otherwise you will have no way of setting the date.

Ok, this now it's clear. But how can I assign bags with delivered_at to users? I've created users and bags, then I've tried: user=User.first, bag=Bag.first Delivery.create(:user_id=>user.id, :bag_id=>bag.id: delivered_at=>'2011-01-01') but when I do user.deliveries I have no data and also for user.bags.

This won't work with the class definitions you post below as the class is Ruser not User.

What happens if you open a ruby console (rails console if using rails 3, script/console otherwise) and enter the lines above (but using the correct class names)?

Colin

Did you work through the Rails Guide on Associations as I suggested? Note particularly << and the .build methods. I would get what you have basically working first in the console as in my previous post.

Colin

Yes, that is fine. In that case you obviously need a Delivery model and a deliveries table, otherwise you will have no way of setting the date.

Ok, this now it's clear. But how can I assign bags with delivered_at to users? I've created users and bags, then I've tried: user=User.first, bag=Bag.first Delivery.create(:user_id=>user.id, :bag_id=>bag.id: delivered_at=>'2011-01-01') but when I do user.deliveries I have no data and also for user.bags.

This won't work with the class definitions you post below as the class is Ruser not User.

Sorry for my mistake but what I've posted is correct, I've write user=User.first but it is user=Ruser.first.

What happens if you open a ruby console (rails console if using rails 3, script/console otherwise) and enter the lines above (but using the correct class names)?

user=Ruser.first bag=Bag.first Delivery.create(:ruser_id=>user.id, :bag_id=>bag.id,:delivery_at=>'2011-01-01') user.deliveries user.bags

Can you post the complete console output please.

Colin

It works if I close and restart console.

Closed and restarted after doing what?

Colin

I do:

irb(main):001:0> user=Ruser.first => #<Ruser id: 82469, year: 2010, code: 100037787, fullname: "zzzzzz.", birthdate: nil, homeaddress: "xxxxxxxxx", homepostalcode: "11111", homelocality: "zzzzzzzz", fiscalcode: "0111111111", contractnumber: "30236", squaremeters: 56, category: 101, taxedaddress: "zzzzzzzzzzz\r\n", created_at: "2011-01-15 17:26:44", updated_at: "2011-01-15 17:26:44">

irb(main):004:0> bag=Bag.first => #<Bag id: 1, bag_type: "secco", created_at: "2011-01-15 23:32:01", updated_at: "2011-01-15 23:32:01">

irb(main):010:0> Delivery.create(:ruser_id=>user.id, :bag_id=>bag.id,:delivery_at=>'2011-01-01') => #<Delivery id: 23, ruser_id: 82469, bag_id: 1, delivery_at: "2011-01-01", created_at: "2011-01-22 16:32:10", updated_at: "2011-01-22 16:32:10">

irb(main):012:0> user.deliveries => irb(main):013:0> user.bags =>

irb(main):014:0> exit

jruby -S rails console

irb(main):002:0> user.deliveries => [#<Delivery id: 23, ruser_id: 82469, bag_id: 1, delivery_at: "2011-01-01", created_at: "2011-01-22 16:32:10", updated_at: "2011-01-22 16:32:10">] irb(main):003:0> user.bags => [#<Bag id: 1, bag_type: "secco", created_at: "2011-01-15 23:32:01", updated_at: "2011-01-15 23:32:01">]

That is interesting, I guess it is because you have already loaded user and have not told it that its deliveries have changed under its feet so to speak. I suspect that if you did user = Ruser.first again it would be ok. If you use the << or build methods then you should not have that problem. You said previously that you had problems finding tutorials with the through construct, if you work through some just using has_many and belongs_to you will get the basic principles and the through bit should not be a problem.

Colin

Thanks for your help.

In this scenario .deliveries and .bags are actually AssociationProxy objects, which have a .reload method. So above, before you exit your console you could have done

user.deliveries.reload user.bags.reload

And you would have gotten the correct collection. As a debugging tool, you can direct your rails logging to stdout (i.e. ActiveRecord::Base.logger = Logger.new(STDOUT)) and then you can see the order of SQL commands. In your example you would have seen the optimistic association loads when you did Ruser.first, and then when you did user.deliveries later you would have noticed no SQL query, indicating that it was using the cached value, and thus indicating you should do the .reload.

As Colin mentions though, you're probably best using user.deliveries <<, as that will both set your association IDs correctly, and have the collection all happy.

HTH, \Peter

About this point I'm still confused. I want delivery one or more bags to ruser and I want to set the date of delivery. I'm thinking: user=Ruser.first; bags=Bag.all user << bags, this create deliveries but set delivered_at == nil. Then I can do user.deliveries.where(:delivered_at => nil).update_all(:delivered_at => Date.today). Can it be a solution?

Sorry I missed the part about the delivered at. You want something like below; note that I added two items to show how the caching affects it:

user = Ruser.first bag1 = Bag.find(...) bag2 = Bag.find(...) d1 = user.deliveries.create(:bag => bag1, :delivered_at => Time.now) user.deliveries # => [d1] user.bags # => [bag1] - Performs the query, finds the bag through the delivery, caches it d2 = user.deliveries.create(:bag => bag2, :delivered_at => 1.day.ago) user.deliveries # => [d1, d2] - Using the association to create made it know about the new one user.bags # => [bag1] - Still cached from above user.bags.reload # => [bag1, bag2] - Forces it to rerun the query