I have the following associations and then action in my Observer:
class Product < ActiveRecord::Base
attr_accessible :price, :name, :watch_price
belongs_to :user
belongs_to :store
has_many :product_subscriptions, :dependent => :destroy
has_many :product_subscribers, :through => :product_subscriptions, :class_name => 'User'
end
class ProductSubscription < ActiveRecord::Base
belongs_to :product
belongs_to :product_subscriber, :class_name => 'User'
attr_accessible :watched_price, :watched_name
end
class ProductObserver < ActiveRecord::Observer
def after_create(product)
ProductSubscription.new(product.attributes.merge({
:watched_name => name,
:watched_price => price,
:store_id => :store_id,
}))
end
end
The code above, successfully creates the ProductSubscription with the user_id and product_id but :watched_name and :watched_price aren't filled with the original Product :price and :name.
I noticed the issue lies in this. Which doesn't make any sense because when I look in the database, it is assigned as I mentioned above:
WARNING: Can't mass-assign protected attributes: product_id
Now I do have other fields that are apart of the Product model that aren't apart of the ProductSubscription model so maybe its screwing up because of that?
I don't want the product_id to be mass assignable. How could I correct this?
Your hash values must reference the attribute methods, not some symbols. That way, the method returning the respective attribute value gets called and the value gets inserted into the hash. The symbols you used have no meaning whatsoever.
ProductSubscription.new(product.attributes.merge({
:watched_name => name,
:watched_price => price,
:store_id => store_id,
}))
end
Also, you don't seem to save your new ProductSubscription. Just calling new won't persist the object to the database. Use something like create instead.
And finally, as Andrew Marshall said, your database design is not really optimal. Copying whole table rows around is not going to offer great performance. Instead you will soon suffer from inconsistencies and the hassles of keeping all the copied data up-to-date. You really should learn about joins and the concepts of Database normalization
Related
I am using Rails 3.2.18. This is a bit of a complex behavior but we have a has_many relationship say the following:
class Item < ActiveRecord::Base
has_many :notes
...
class Note < ActiveRecord::Base
belongs_to :item
attr_accessor :body, :is_enabled
What I'd like to do is when a method does:
#note=Note.find(2)
the item object (or possibly the note object) can do a check before establishing the relationship. Specifically, in our body we have a bunch of content that must be checked for all the items to be valid (say we use a phrase like ITEM2, ITEM17 for item #2 or item #17). If any of the items are invalid, the the note should have is_enabled set to false and not be loaded. How can I check the validity of ITEM2 before the note is loaded? I was hoping for a callback or perhaps a condition which contains a macro like:
has_many :notes, Proc.new { I don't know what to put here? }
Considering is_enabled flag is set in database, you may define multiple associations like:
has_many :notes #This will retrieve all notes
has_many :enabled_notes, :conditions => {:is_enabled => true}
If I have a user and article model with an association has_many :articles and belongs_to :user, I would write user.articles.new to create a new article object with the correct user_id .
So my question is about a model with many belongs_to relations:
class Ownership < ActiveRecord::Base
attr_accessible :right_read, :right_create, :right_update, :right_delete
belongs_to :element
belongs_to :user
belongs_to :ownership_type
end
Is there a solution to create an object Ownership with the 3 IDs completed (element_id, user_id, ownership_type_id) ?
And is it dangerous to write this IDs in the "attr_accessible" ?
Thank you.
The new method accepts a hash where the keys match the attributes in the model. This should work just fine:
Ownership.new(:element_id => element_id, :user_id => user_id, :ownership_type_id => ownership_type_id)
Reference: http://apidock.com/rails/ActiveRecord/Base/new/class
Also, no, it's not dangerous to include those attributes under attr_accessible -- actually, that's the only way you'll be able to directly write to them using new or update_attributes.
I'm working on a Rails app (Ruby 1.9.2 / Rails 3.0.3) that keeps track of people and their memberships to different teams over time. I'm having trouble coming up with a scalable way to combine duplicate Person objects. By 'combine' I mean to delete all but one of the duplicate Person objects and update all references to point to the remaining copy of that Person. Here's some code:
Models:
Person.rb
class Person < ActiveRecord::Base
has_many :rostered_people, :dependent => :destroy
has_many :rosters, :through => :rostered_people
has_many :crews, :through => :rosters
def crew(year = Time.now.year)
all_rosters = RosteredPerson.find_all_by_person_id(id).collect {|t| t.roster_id}
r = Roster.find_by_id_and_year(all_rosters, year)
r and r.crew
end
end
Crew.rb
class Crew < ActiveRecord::Base
has_many :rosters
has_many :people, :through => :rosters
end
Roster.rb
class Roster < ActiveRecord::Base
has_many :rostered_people, :dependent => :destroy
has_many :people, :through => :rostered_people
belongs_to :crew
end
RosteredPerson.rb
class RosteredPerson < ActiveRecord::Base
belongs_to :roster
belongs_to :person
end
Person objects can be created with just a first and last name, but they have one truly unique field called iqcs_num (think of it like a social security number) which can be optionally stored on either the create or update actions.
So within the create and update actions, I would like to implement a check for duplicate Person objects, delete the duplicates, then update all of the crew and roster references to point to the remaining Person.
Would it be safe to use .update_all on each model? That seems kind of brute force, especially since I will probably add more models in the future that depend on Person and I don't want to have to remember to maintain the find_duplicate function.
Thanks for the help!
The 'scalable' way to deal with this is to make the de-duplication process part of the app's normal function - whenever you save a record, make sure it's not a duplicate. You can do this by adding a callback to the Person model. Perhaps something like this:
before_save :check_for_duplicate
def check_for_duplicate
if iqcs_num
dup = Person.find_by_iqcs_num(self.iqcs_num)
if dup && dup.id != self.id
# move associated objects to existing record
dup.crews = dup.crews + self.crews
# update existing record
dup.update_attributes(:name => self.name, :other_field => self.other_field)
# delete this record
self.destroy
# return false, so that no other callbacks get triggered
return false
end
end
end
You'll want to make sure that you index the table you store Person objects in on the iqcs_num column, so that this lookup stays efficient as the number of records grows - it's going to be performed every time you update a Person record, after all.
I don't know that you can get out of keeping the callback up-to-date - it's entirely likely that different sorts of associated objects will have to be moved differently. On the other hand, it only exists in one place, and it's the same place you'd be adding the associations anyway - in the model.
Finally, to make sure your code is working, you'll probably want to add a validation on the Person model that prevents duplicates from existing. Something like:
validates :iqcs_num, :uniqueness => true, :allow_nil => true
I have a Family class that includes a mother_id and a father_id. From the Family model's perspective, it's important to know which parent is the mother and which is the father, but the mother and father as Residents have all the same attributes (i.e. database columns). So ideally, I'd like my model files to look like this:
class Resident < ActiveRecord::Base
has_one :family, :dependent => :nullify, :foreign_key => :father_id
has_one :family, :dependent => :nullify, :foreign_key => :mother_id
attr_accessible :email, :cell, :first_name, :last_name
end
class Family < ActiveRecord::Base
belongs_to :father, :class_name => 'Resident', :foreign_key => 'father_id'
belongs_to :mother, :class_name => 'Resident', :foreign_key => 'mother_id'
attr_accessible :address, :city, :state, :number_of_children
end
This doesn't work. my_family.mother and my_family.father work, so Rails seems to be happy with the double belongs_to. However, my_dad.family == nil, indicating that the second has_one is overriding the first. This is reasonable, because otherwise, what would happen if a resident_id showed up in both the mother_id and father_id columns? (While I plan to add model-level validation to ensure that never happens, has_one doesn't talk to validation methods.) Furthermore, what would my_dad.family = Family.new mean? How would ActiveRecord choose whether to insert my_dad.id into Family.mother_id or Family.father_id?
From this Stackoverflow question, I got the idea to use different names, i.e. change the has_one lines to:
has_one :wife_and_kids, :class_name => 'Family', :dependent => :nullify, :foreign_key => :father_id
has_one :husband_and_kids, :class_name => 'Family', :dependent => :nullify, :foreign_key => :mother_id
My questions are:
1) Is there a better way to do it? A different DB schema, perhaps?
2) Is database-level validation possible to supplement the model-level validation to ensure that my_dad.id can't show up in both the mother_id and father_id columns?
3) Can you think of better names than husband_and_kids / wife_and_kids? (Admittedly not a programming question...)
EDIT:
It occurred to me to add a family getter:
def family
#family ||= self.wife_and_kids || self.husband_and_kids
end
after_save :reset_family
def reset_family
#family = nil
end
This makes it syntactically cleaner (since I really wasn't a fan of [husband|wife]_and_kids), without creating any ambiguity since there's no setter.
The main issue you're facing is that you have a "conditional" foreign key, meaning the foreign key used to resolve the :family of a resident depends on whether the resident is a male or female (mother or father). The best way to deal with this in my opinion is to use STI (Single-Table Inheritance) to differentiate between the two cases.
class Resident < ActiveRecord::Base
attr_accessible :email, :cell, :first_name, :last_name
end
class Mother < Resident
has_one :family, :dependent => :nullify, :foreign_key => :mother_id
end
class Father < Resident
has_one :family, :dependent => :nullify, :foreign_key => :father_id
end
You can still use the Resident table, but you'll need to migrate a :type field of type string and store the value "Mother" or "Father" depending on the case. Also, place each of these class definitions in its own file in models/.
Edit: I think this also resolves the issues suggested in your second and third questions.
Edit2:
Given the current schema, you would need to create a check constraint on your families table. For one, active record doesn't have direct support for this, so you would have to execute raw sql to add the constraint. In theory, each time a value is added or changed in the "mother_id" column of "families", the check would have to cross reference with the "residents" table, ascertaining that the "type" column of the "resident" is "Mother." The SQL that would (theoretically) add this constraint is
ALTER TABLE families
ADD CONSTRAINT must_be_mother CHECK ((SELECT type FROM residents WHERE residents.id = families.mother_id) = 'Mother')
The problem is that this CHECK contains a subquery, and as far as I know, subqueries in checks are disallowed by many databases. (See this question for specifics).
If you really want to implement a database-level validation here, you will likely need to change the schema by separating "residents" into "mothers" and "fathers."
I would like to build a model where a class of ServiceRegions has a many-to-many relationship with zip codes. That is, ServiceRegions might cover multiple zip codes, and they might overlap, so the same zip code could be associated with multiple ServiceRegions.
I was hoping to store the zip code directly in a relationship table rather than creating a ZipCode class, but I can't get the code to work properly. I successfully got code to create relationships, but I was unable to access an array of associated zips as one would expect to be able to.
Here's the relevant code:
class ServiceRegion < ActiveRecord::Base
has_many :z_sr_relationships, :dependent => :destroy,
:foreign_key => :service_region_id
has_many :zips, :through => :z_sr_relationships, :source => :zip
def includes_zip!(zip)
z_sr_relationships.create!( :zip_id => zip, :service_region_id => self.id)
end
end
class ZSrRelationship < ActiveRecord::Base
attr_accessible :service_region_id, :zip
belongs_to :service_region, :class_name => "ServiceRegion"
validates :zip, :presence => true
validates :service_region_id, :presence => true
end
When I do a show on an instance of a ServiceRegion and try to output my_service_region.zips it gives me an error that it can't find the association zips.
Is Rails meant to let you do a many to many association with a basic type like a string or an int that's not a defined class with its own model file?
Any association: has_many, belongs_to, has_many :though etc., need to relate to subclasses of active record. Objects that aren't a descendent of AR wouldn't have the database backing to relate to AR objects.
I think you're getting the "can't find association" error because you're specifying :source => :zip. You'd need to have a class called Zip. You have a class called ZSrRelationship, which is what rails expects, so you should probably just leave the source option out.