has_many :through -- Adding metadata to the through relationship - ruby-on-rails-3

I have a need to add metadata about a HABTM relationship. I wanted to use a has_many :through relationship to accomplish this, but it is not necessary. Here is the problem simplified:
class Customer < ActiveRecord::Base
has_many :customer_teddy_bears
has_many :teddy_bears, :through => :customer_teddy_bears
end
class CustomerTeddyBear < ActiveRecrod::Base
belongs_to :customer
belongs_to :teddy_bear
attr_accesible :most_favoritest # just to show it exists, boolean
end
class TeddyBear < ActiveRecord::Base
has_many :cusomter_teddy_bears
end
So what I need to do is start adding teddy bears to my customers, Teddy Bears are a fixed set of data, lets say a fireman_bear, doctor_bear, dominatrix_bear. Any customer can claim to own a kind of teddy bear, but they also specify which is their most favoritest bear. Since I cannot modify the bears model because that is globally shared among all customers I am adding the metadata (among other metadata) to CustomerTeddyBear.
The problem is that the following does not work.
customer = Customer.new # new record, not yet saved, this must be handled.
customer.teddy_bears << fireman_bear
customer.teddy_bears << doctor_bear
# now to set some metadata
favoritest_record = customer.customer_teddy_bears.select{|ctb| ctb.teddy_bear == doctor_bear}.first
favoritest_record.most_favoritest = true
The above code does not work since customer_teddy_bears entries are only populated during save when creating records in the database. Is there another mechanism for doing this?
If there is nothing "automated" built into rails I will just have to manually manage this relationship by including teddy_bears when I select customer_teddy_bears and using techniques like
def teddy_bears
self.customer_teddy_bears.map(&:teddy_bear)
end
along with manually creating the associations, and not using a :through relationship.
please note, all this must happen before the #save is executed on the Customer object, so I need to set all relevant metadata while still in-memory.
Recommendations I got from #RubyOnRails
ctb = customer.customer_teddy_bears.build({:customer => customer, :teddy_bear => fireman_bear})
ctb2 = customer.customer_teddy_bears.build({:customer => customer, :teddy_bear => doctor_bear})
...
ctb.most_favoritest = true

You can simply do this:
customer = Customer.new # new record, not yet saved, this must be handled.
customer.teddy_bears << fireman_bear
customer.teddy_bears << doctor_bear
customer.save
fav = CustomerTeddyBear.where(:customer_id => customer.id, :teddybear_id => doctor_bear.id)
fav.most_favoritest = true
fav.save

The solution I was forced to resort to is manually building the CustomerTeddyBear object and setting both the customer, teddy_bear, and most_favoritest. Basically most of the time, access is by customer.customer_teddy_bears.map(&:teddy_bear) at least in logic where the possibility is that the record is not yet saved, otherwise just short-cut to customer.teddy_bears.

Related

Returning associations for specific model when there is a polymorphic association in Rails 3.2

I have a polymorphic association in a Rails 3 app where a User may favorite objects of various classes.
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :favoriteable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :favorites
end
class Image < ActiveRecord::Base
has_many :favorites, :as => :favoriteable
end
class Video < ActiveRecord::Base
has_many :favorites, :as => :favoriteable
end
I would like to be able return a list of just a User's favorite_images for example.
user.favorite_images #returns a list of the images associated with the user via :favoritable
I'm guessing there is a straightforward way of doing this but I haven't been able to figure it out. Let me know if you need anymore info.
Thanks!
===edit====
I know that I could retrieve what I am looking for via
favorite_images = user.favorites.collect{|f| if f.favoriteable_type=='Image'; f.favoriteable; end}
I could define an instance method for the User class and put that inside. I was wondering if there is a way to do it as some sort of has_many association. Really just because going forward it would be easier to have all that in one place.
When you created the table for Favorite you created a couple of columns favoriteable_id and favoriteable_type and you can use this information to restrict your query.
If you do user.favorites you will get all of the favorites and to restrict them to say just the images then you can do user.favorites.where(favoriteable_type: 'image') but that just gives you the favorite records and it sounds like you want the actual images. To get those you can do it by then mapping and pulling the favoriteable out. You'll likely want to include it in the query though so you don't hit the database so much. I would also make this a method on User.
def favorite_images
favorites.includes(:favoriteable).where(favoriteable_type: 'Image').map(&:favoriteable)
end

Rails: How do I transactionally add a has_many association to an existing model?

Let's imagine I run an imaginary art store with a couple models (and by models I'm referring to the Rails term not the arts term as in nude models) that looks something like this:
class Artwork < ActiveRecord::Base
belongs_to :purchase
belongs_to :artist
end
class Purchase < ActiveRecord::Base
has_many :artworks
belongs_to :customer
end
The Artwork is created and sometime later it is included in a Purchase. In my create or update controller method for Purchase I would like to associate the new Purchase with the existing Artwork.
If the Artwork did not exist I could do #purchase.artworks.build or #purchase.artworks.create, but these both assume that I'm creating a new Artwork which I am not. I could add the existing artwork with something like this:
params[:artwork_ids].each do |artwork|
#purchase.artworks << Artwork.find(artwork)
end
However, this isn't transactional. The database is updated immediately. (Unless of course I'm in the create controller in which case I think it may be done "transactionally" since the #purchase doesn't exist until I call save, but that doesn't help me for update.) There is also the #purchase.artwork_ids= method, but that is immediate as well.
I think something like this will work for the update action, but it is very inelegant.
#purchase = Purchase.find(params[:id])
result = #purchase.transaction do
#purchase.update_attributes(params[:purchase])
params[:artwork_ids].each do |artwork|
artwork.purchase = #purchase
artwork.save!
end
end
This would be followed by the conventional:
if result
redirect_to purchase_url(#purchase), notice: 'Purchase was successfully updated.' }
else
render action: "edit"
end
What I'm looking for is something like the way it would work from the other direction where I could just put accepts_nested_attributes_for in my model and then call result = #artwork.save and everything works like magic.
I have figured out a way to do what I want which fairly elegant. I needed to make updates to each part of my Product MVC.
Model:
attr_accessible: artwork_ids
I had to add artwork_ids to attr_accessible since it wasn't included before.
View:
= check_box_tag "purchase[artwork_ids][]", artwork.id, artwork.purchase == #purchase
In my view I have an array for each artwork with a check_box_tag. I couldn't use check_box because of the gotcha where not checking the box would cause a hidden value of "true" to be sent instead of an artwork id. However, this leaves me with the problem of deleting all the artwork from a purchase. When doing update, if I uncheck each check box, then the params[:purchase] hash won't have an :artwork_ids entry.
Controller:
params[:purchase][:artwork_ids] ||= []
Adding this guarantees that the value is set, and will have the desired effect of removing all existing associations. However, this causes a pesky rspec failure
Purchase.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) fails because :update_attributes actually received {"these"=>"params", "artwork_ids"=>[]}). I tried setting a hidden_value_tag in the view instead, but couldn't get it to work. I think this nit is worthy of a new question.
It is probably best to use make the purchase model a join table and have many to many associations.
Here is an example for your use case.
Customer model
has_many :purchases
has_many :artwork, :through => :purchase
Artwork model
has_many :purchases
has_many :customers, :through => :purchase
Purchase model
belongs_to :customer
belongs_to :artwork
The purchase model should contain customer_id and artwork_id.
you would also need to create a purchase controller that allows you create a new purchase object.
When a customer presses the purchase button it would create a new purchase object which includes the customer_id and the artwork_id. This allows you to create an association between the customer and the artwork they purchase. You can also have a price_paid column to save the price the customer paid at the time of purchase.
if you need more help you can research join many to many associations using :through.
hope it helps

has_many :through and build

I have three models, Account, User and Contact:
class User < ActiveRecord::Base
has_one :account
has_many :contacts, :through => :account
end
class Account < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :account
end
I'm trying to scope build a new contact through the user record, like this in my contacts controller.
def create
#contact = current_user.contacts.build(params[:contact])
respond_to do |format|
if #contact.save
...
else
...
end
end
end
When I do this, I don't receive any errors, the contact record is saved to the database however the account_id column is not set on the contact, and it is not added to the collection so calling #current_user.contacts returns an empty collection.
Any suggestions?
Using build makes a new instance of Contact in memory, but you would need to manually set the account_id on the record (e.g. #contact.account_id = current_user.account.id), or perhaps set it in a hidden field in the new form used to display the contact for creation such that it is picked up in the params array passed to the build method.
You might also want to consider whether accepts_nested_attributes_for may be helpful in this case. Another option may be to use delegate, although in both cases, your use may be sort of the opposite of what these are intended for (typically defined on the "parent").
Update:
In your case, the build method is added to both the User instance and to the Account (maybe "Owner") instance, because you have both a many-to-many relationship between User and Contact, as well as a one-to-many relationship between Account and Contact. So to get the account_id I think you would need to call Account's build, like
#contact = current_user.accounts.contacts.build(params[:contact])
Does this work?

Using attr_accessible in a join model with has_many :through relationship

I have a USER that creates a COMPANY and become an EMPLOYEE in the process. The employees table has an :user_id and a :company_id.
class User
has_many :employees
has_many :companies, :through => :employees
class Employee
belongs_to :user
belongs_to :company
attr_accessible :active
class Company
has_many :employees
has_many :users, :through => employees
Pretty basic. But here's the thing, the resource EMPLOYEE has other attributes than its foreign keys, like the boolean :active. I would like to use attr_accessible, but this causes some problems. The attribute :user_id is set right, but :company_id is nil.
#user.companies << Company.new(...)
Employee id:1 user_id:1 company_id:nil
So my question is: if :user_id is set right, despite it is not an attr_accessible, why :company_id isn't set right just the same? It shouldn't be an attr_accessible.
I'm using Rails 3.0.8, and have also tested with 3.0.7.
There are a lot of bits working together here.
You definitely want to use attr_accessible on all models. (Google "hack rails mass assignment" and read the Rails Guide on mass assignment.)
Once you add attr_accessible to a model, all assignments from hashes (mass assignments) are disabled except those you explicitly allow. However, you can still assign values directly, one at a time.
Foreign keys seem like a good thing to exclude from mass assignment, so don't list them in attr_accessible.
The .create and .build methods are not using mass assignment so they can set the value of one foreign key association. If there are several associations, as best I can tell, you'll have to set all but the first separately.
Finally, the actual IDs for the foreign keys are created by the database, not by ActiveRecord. So you'll either have to create parent and child records simultaneously, or you'll have to save the child first before you can assign the foreign key in the parent. Otherwise there is no ID available for the assignment.
It's not clear to me from your example how Employee is getting instantiated. But since the Employee belongs to both User and Company, I think something like this might work, assuming #user already exists:
company = #user.companies.create(..) # fills in company.user_id and saves to DB
employee = #user.employees.build(..) # fills in employee.user_id but does NOT save yet
employee.company = company # fills in employee.company_id
employee.save # now save to DB
The company_id is nil simply because the Company hasn't been saved to the database yet - Company.new simply creates the object in memory without saving it yet.
If you do:
#user.companies << Company.create(..)
or
#user.companies << Company.first
They should both work. There's even a shorter method which I think should work too:
#user.companies.create(..)
It all depends at which point you want to save the association. In some cases, it may be better not to save the employee and company models straight away, and instead wait for when the parent model (User) is saved. In which case you can use:
#user.companies.build(..)
(which is similar to the code in your example).
In terms of your active boolean attribute on the Employee model, if this is a column in the database, you don't need to explicitly declare attr_accessible for it - it'll be accessible by default.

Rails: Make different references to a DB row refer to the same Ruby object

Suppose I have the following model relationship:
class Player < ActiveRecord::Base
has_many :cards
end
class Card < ActiveRecord::Base
belongs_to :player
end
I know from this question that Rails will return me a copy of the object representing a database row, meaning that:
p = Player.find(:first)
c = p.cards[0]
c.player.object_id == p.object_id # => false
...and therefore if the Player model modifies self, and the Card model modifies self.player in the same request, then the modifications won't take any notice of each other and the last-saved one will overwrite the others.
I'd like to work around this (presumably with some form of caching), so that all requests for a Player with a given id would return the same object (identical object_ids), thereby allowing both models to edit the same object without having to perform a database save-and-reload. I have three questions:
Is there already a plugin or gem to do this?
Are there good reasons why I shouldn't do this?
Can anyone give me some pointers on how to go about doing this?
This is supported in Rails 3.x. You can use the :inverse_of option for the has_many association for example. Documentation here (search for :inverse_of and Bi-directional associations).