Rails has_many assocation with non-class? - ruby-on-rails

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.

Related

Related object not saved

I have the models:
class Idea < ActiveRecord::Base
has_many :connections, :class_name => 'IdeaConnection', :foreign_key => 'idea_a_id', :dependent => :destroy
has_many :ideas, :through => :connections, :source => :idea_b, :dependent => :destroy
end
class IdeaConnection < ActiveRecord::Base
belongs_to :idea
belongs_to :idea_a, :class_name => 'Idea'
belongs_to :idea_b, :class_name => 'Idea'
belongs_to :relationship
end
class Relationship < ActiveRecord::Base
has_many :idea_connections
end
Idea, as you can see, own itself through Connections (join table). Each Connection entry belongs to Relationship. What I'm trying to do is to, after adding an Idea to another with:
Idea.find(1).ideas << Idea.find(2)
which is working and saving properly, get its connection on join table and update its relationship:
Ex:
Idea.find(1).connections.find_by_idea_b_id(Idea.f
ind(2).id).relationship = Relationship.find(1)
It processes correctly but it won't save.
Please, help, what am I missing?
ps: I don't want to do it by manually editting the relationship_id since it's ugly.
ps2: Before you answer, remember the fact that autosave:true do not work for belongs_to/has_many relationships.
You're working with connection object. Remember it.
Your problem is that you call find_by... method in has_many association. It returns one record BUT Idea model has no ruby attribute link to that. See here why. That's why Idea#save cannot call IdeaConnection#save (remember that for cascade saving connections realtion must have autosave: true if connection already exists).
So I suggest you two options:
Set :inverse_of on Idea#connections and IdeaConnection#idea_a relations and preload all records before mangling with Idea.find(1).connections.find_by_idea_b_id(Idea.f
ind(2).id).relationship = Relationship.find(1). But I don't recommend you to do so because:
As I said you're working with connection object. Just do so:
Idea.find(1).connections.create do |connection|
connection.idea_b = Idea.find(2)
connection.relationship = Relationship.first
end
This line of code won't save it
Idea.find(1).connections.first.relationship = Relationship.first
What you need to do is:
Method #1: ( add the connection to the has_many relation )
Relationship.first.idea_connections << Idea.find(1).connections.first
OR
Method #2: ( add relationship_id to the connection then manually save it)
connection = Idea.find(1).connections.first
connection.relationship_id = Relationship.first.id
connection.save
Assignments on the relational object level don't/won't automatically save; you have to tell them to. In ActiveRecord the push method (<<) has save built into it, which is why that was working for you. Setting a value (=) however does not have save built in, so you have to do it manually.
If you're interested, here's a link to another SO question where an answer talks about why push saves: Rails push into array saves object

has_one with more than one possible foreign key column

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."

Unable to copy another model's attributes correctly?

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

Rails 3: What associations should I use to create model relationships

I am creating an app for uploading and sharing files between users.
I have User and Files models and have created a third File_Sharing_Relationships model which contains a sharer_id, file_id and shared_with_id columns. I want to be able to create the following methods:
#upload.file_sharing_relationships - lists users that the file is shared with
#user.files_shared_with - lists files that are shared with the user.
#user.files_shared - lists files that the user is sharing with others
#user.share_file_with - creates a sharing relationship
Are there any rails associations, such as 'polymorphic' that I could be using to make these relationships?
Any suggestions appreciated. Thanks.
All you need to do is to read Rails Guides and apply all what you learn.
Basically you need to store info about:
user who created a "sharing"
user or group or whatever is a target of a sharing action
resource that is being shared
So:
class SharedItem < ActiveRecord::Base
belongs_to :sharable, :polymorphic => true #this is user, please think of better name than "sharable"...
belongs_to :resource, :polymorphic => true #can be your file
belongs_to :user
end
You need SharedItem to have:
user_id: integer, sharable_id: integer, sharable_type: string, resource_id: integer, resource_type: string
Then you can get "methods" you specified by writing named scopes like:
named_scope :for_user, lambda {|user| {:conditions => {:user_id => user.id} }}
or by specifying proper associations:
class File < ActiveRecord::Base
has_many :shared_items, :as => :resource, :dependent => :destroy
end
I think you should create relationships something like this:
class User
has_many :files
has_many :user_sharings
has_many :sharings, :through => :user_sharings
end
class File
belongs_to :user
end
class Sharing
has_many :user_sharings
has_many :users, :through => :user_sharings
end
class UserSharing
belongs_to :user
belongs_to :sharing
end
.. this is very basic model of relations(It is just my point of view :)). User can have many sharings and also belongs to sharings. You can set file id to UserSharing table when you create user and it's share. And then you can create methods, you listed above, as scopes in proper models. I hope I helped you a little.

How to destroy a record with has_many, :dependent => :destroy

I've built a Rail 3 AuditLog with the help of a few plugins, that store data in an AuditLog Table with the following fields for identification (feeded_id, feeded_type)
So in my case, I have a photoalbum that has_many photos.
class PhotoAlbum < ActiveRecord::Base
has_many :photos, :dependent => :destroy
when I delete a photoalbum (id=2) this works very well to delete all associated photos, but it doesn't delete items from the AuditLog that are like this: (feeded_id = 2, feeded_type = PhotoAlbum)
Given that the AuditLog table doesn't have a "photo_album_id" column, and can't, is there a way to setup a dependent > Destory with Rails to delete all associated items in teh AuditLog when a PhotoAlbum is deleted?
Thanks, I know this one's a little more complicated than most. Thanks for reading through it!
I think what you are looking for is the combination of
belongs_to :feeded, :polymorphic => true
in your Audit log class and
has_many :logs, :as => :feeded, :dependent => :destroy
in your PhotoAlbum class.
If you do not have a class to represent your audit log, you should be able to add the belongs_to to the existing class (in your plugins perhaps?).
I'm not 100% sure about the :as => :feeded option, you will have to name that symbol correctly and I am not sure what ActiveRecord will expect, but the belongs_to relationship will look for feeded_id and feeded_type, so when the 'parent' object is a PhotoAlbum it will join correctly on photo_album.id = audit_logs.feeded_id AND audit_logs.feeded_type = 'PhotoAlbum'. Since this doesn't require any changes to your database, all your existing code should continue to work.
You can read up on the options for associations here.

Resources