I have an polymorphic model called Address. What I'm trying to achieve is to havy many addresses binded to other models (for example User), but still use them like if there was only one, and try to distinguish them by type.
My project was designed to use only one address, so i have a lot pieces of code like User.first.address or User.first.address=... which i can do nothing about.
Here is sort of example:
|id|addressable_type|addressable_id| type | street |
1|User |1 | first | Foo
2|User |1 | second | Bar
3|User |1 | NULL | Other
And what i want to do is:
User.find(1).address(:second)
Which should return <Address street:"Bar">,
User.find(1).address
Which should return <Address street:"Other">,
but User.find(1).addresses should return collection of objects.
I work with a piece of code to add address behaviour to my models:
module Ext::Models::Addressable
def self.included(mod)
super
mod.class_eval do
def self.acts_as_addressable
include InstanceMethods
# Use has_many :delete_all for better performance
has_many :addresses, :as => :addressable, :dependent => :delete_all
has_one :address, :as => :addressable
end
end
end
module InstanceMethods
# I tried with this, but this doesn't even allow me to start rails becauses raises "there is no address method"
def address_with_type(type = nil)
if type
self.addresses.find(:first, :conditions => ["type = ?", type.to_s])
else
address_without_type
end
end
alias_method_chain :address, :type
end
end
I'm using Rails 2.3.14 and Ruby 1.9.2.
If this is possible i also'd like to know what is best way to create new addresses for given model (lets say User).
Thanks in advance.
Solution
I narrowed down all the possibilites, and currently in project there are only two types of addresses. I was able to simplify my approach a lot. Currently my self.included looks like:
has_many :addresses, :as => :addressable, :dependent => :delete_all
has_one :address, :as => :addressable, :conditions => {:type => nil}
has_one :foo_address, :class_name => 'Address', :as => :addressable, :conditions => {:type => "foo"}
And now i'm able to build new object like this:
user.build_address(:street => "abc") and user.build_foo_address(:street => "def"), what is exacly what I need.
I would be a bit careful about overriding default and expected behaviour in that way. Is there a reason you cant let user.address be?
I'm not exactly sure what problems you are solving but maybe you could do something like:
class User < ActiveRecord::Base
has_many :addresses, :as => :addressable, :dependent => :delete_all
def address(only_type = "primary")
addresses.find_first("type" => only_type)
end
def address=(addr)
addresses.delete address
addresses << addr.update_attribute("type", "primary")
end
def build_address(attr)
addresses.delete address
addresses.build attr.merge("type" => "primary")
end
def create_address(attr)
addresses.delete address
addresses.create attr.merge("type" => "primary")
end
end
I haven't tested this code and I'm not that familiar with rails 2 any more so just use it as inspiration. You could wrap it in some meta stuff to modularize it. Btw, if you are using "type" you get STI, not sure if that is what you want.
Related
I have this relation in my Product model:
has_many :features, :class_name => 'ProductFeature', :source => :product_feature, :include => :feature
So I can do Product.features
which works fine. But I want to be able to filter that by fields in the feature table, when and if necessary. For example in pseudo code:
find all product features where feature is comparable
compare is a bool field on the feature.
I have been trying for 2 hours solid and cannot figure it out (without writing a new query completely). I can't figure out how to access the feature table's fields from the Product.features relation, as it seems it can only filter on product_features fields.
This is what I have come up with so far:
def features_compare
features.feature.where(:compare => true)
end
But it just says feature is not a valid method, which I understand.
Edit
I have updated my model so the relationships are clearer:
product.rb:
class Product < ActiveRecord::Base
belongs_to :company
belongs_to :insurance_type
has_many :product_features
has_many :reviews
attr_accessible :description, :name, :company
end
product_feature.rb:
class ProductFeature < ActiveRecord::Base
belongs_to :product
belongs_to :feature
delegate :name, :to => :feature
attr_accessible :value
end
feature.rb
class Feature < ActiveRecord::Base
attr_accessible :name, :compare
end
I want to be able to query the product_features that belong to a product and feature where Feature.compare is true. Something like this:
product.rb
def features_compare
product_features.where(:compare => true)
end
This throws an error because compare in in the Feature model, not ProductFeature. I have tried the following in product_feature.rb:
delegate :compare, :to => :feature
but I didn't help.
I will adding a bounty to this in a few hours so please please help me!
find all product features where feature is comparable is just
ProductFeature.joins(:feature).where(:feature => {:compare => true})
You can make that a bit more reusable by introducing a scope:
#in product_feature.rb
scope :with_feature_like, lambda do |filter|
joins(:feature).where(:feature => filter)
end
#elsewhere
ProductFeature.with_feature_like(:compare => true)
#all the product features of a certain product with at comparable features
some_product.product_features.with_feature_like(:compare => true)
Finally, if you want all products with product features with comparable features, you want something like:
Product.joins(:product_features => :feature).where(:feature => {:compare => true})
which of course you can also turn into a scope on Product.
This seems like a has_many :through relationship. Try changing this:
has_many :features, :class_name => 'ProductFeature', :source => :product_feature, :include => :feature
to this:
has_many :product_features
has_many :features, :through => :product_features
As long as your ProductFeature model has this:
belongs_to :product
belongs_to :feature
And you have the appropriate columns on product_features (product_id, feature_id), then you should be able to access that product's features and all the attributes on both Product and ProductFeature.
See here:
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
EDIT: Here's how to filter by feature fields.
Product.joins(:features).where(:features => {:name => "Size"})
#product.each |p| { p.features.where(:comparable => true) } is probably your best bet here, but I'm open to being enlightened.
I've two tables which are related say A and B. A has has_many to B and B belongs_to A. However I've a field in A stored say A.account_number. And A is totally unrelated to table C which is accounts table. D has account details like addresses and other details. C has has_many relation to D and D belongs_to C. Now using acts_as_api in A and B . I wrote a big query which almost fetches every field I need except for the account and acoount details. How do I get this details using acts_as_api. I tried using calling scopes sub resource method. but it did not work. Any ideas. Please share. I'm new to rails. Here is my code.
Let's Say
A-> item_people
B-> item_people_roles
C-> people_accounts
D-> people_account_details
Class ItemPeople
class ItemPeople < ActiveRecord::Base
has_many:item_people_roles, class_name => "ItemPeopleRole", :dependent => :destroy
accepts_nested_attributes_for :item_people_roles, :allow_destroy => true, :reject_if => :all_blank
acts_as_api
api_accessible :bill_rewriting do |bill|
bill.add :account_number
bill.add :item_people_roles
end
end
Class ItemPeopleRoles
class ItemPeopleRole < ActiveRecord::Base
attr_accessor :messages
belongs_to :item_people, :class_name => "ItemPeople"
acts_as_api
api_accessible :bill_rewriting do |bill|
bill.add :item_people_id, :as => :shipper_id, :if => lambda{|u|u.role_type_code=="SHIPPER"}
bill.add :item_people_id, :as => :consignee_id, :if => lambda{|u|u.role_type_code=="CONSIGNEE"}
bill.add :item_people_id, :as => :ship_to_id, :if => lambda{|u|u.role_type_code=="SHIPTO"}
end
end
Class C People
class People < ActiveRecord::Base
# This model has account number
# account type fields
end
Class D People_Details
class PeopleDetails < ActiveRecord::Base
# This model has address1, address2, name1, name2
end
Now according to people's role there in the itempeopleroles, I need to get people and people details fields in acts_as_api of ItemPeople. Hope I'm clear now
The best way to accomplish this is via a template Proc/lambda
api_accessible :public do |template|
template.add lambda { |record,options|
# look at options, and use those to build your custom data
return [the correct data]
}, :as => :a_magic_field
end
Then in your controller, get a results set, "annotate" it, and then render it.
raw_results = Model.where(... some where conditions ....)
results = raw_results.as_api_response(:public, ... [some other options to appear in your lambda])
And finally render it:
respond_to do |format|
format.json {render :json => results, :callback => params[:callback]}
format.xml {render :xml => results}
end
With this, you can write any code you want inside your lambda to navigate your data structures, regardless of how it is laid out. How it will perform is another question....
(I can't add comments yet).
If A has A.account_number and C is the table of accounts, why don't you have associations between A and C (and maybe even A and D as well as C and D)?
My "answer" is to add those associations and then just use the proper :include syntax. I don't really see what acts_as_api is providing.
I've run into a situation that I am not quite sure how to model.
EDIT: The code below now represent a working solution. I am still interested in nicer looking solutions, though.
Suppose I have a User class, and a user has many services. However, these services are quite different, for example a MailService and a BackupService, so single table inheritance won't do. Instead, I am thinking of using polymorphic associations together with an abstract base class:
class User < ActiveRecord::Base
has_many :services
end
class Service < ActiveRecord::Base
validates_presence_of :user_id, :implementation_id, :implementation_type
validates_uniqueness_of :user_id, :scope => :implementation_type
belongs_to :user
belongs_to :implementation, :polymorphic => true, :dependent => :destroy
delegate :common_service_method, :name, :to => :implementation
end
#Base class for service implementations
class ServiceImplementation < ActiveRecord::Base
validates_presence_of :user_id, :on => :create
#Virtual attribute, allows us to create service implementations in one step
attr_accessor :user_id
has_one :service, :as => :implementation
after_create :create_service_record
#Tell Rails this class does not use a table.
def self.abstract_class?
true
end
#Name of the service.
def name
self.class.name
end
#Returns the user this service
#implementation belongs to.
def user
unless service.nil?
service.user
else #Service not yet created
#my_user ||= User.find(user_id) rescue nil
end
end
#Sets the user this
#implementation belongs to.
def user=(usr)
#my_user = usr
user_id = usr.id
end
protected
#Sets up a service object after object creation.
def create_service_record
service = Service.new(:user_id => user_id)
service.implementation = self
service.save!
end
end
class MailService < ServiceImplementation
#validations, etc...
def common_service_method
puts "MailService implementation of common service method"
end
end
#Example usage
MailService.create(..., :user => user)
BackupService.create(...., :user => user)
user.services.each do |s|
puts "#{user.name} is using #{s.name}"
end #Daniel is using MailService, Daniel is using BackupService
Notice that I want the Service instance to be implictly created when I create a new service.
So, is this the best solution? Or even a good one? How have you solved this kind of problem?
I don't think your current solution will work. If ServiceImplementation is abstract, what will the associated classes point to? How does the other end of the has_one work, if ServiceImplementation doesn't have a pk persisted to the database? Maybe I'm missing something.
EDIT: Whoops, my original didn't work either. But the idea is still there. Instead of a module, go ahead and use Service with STI instead of polymorphism, and extend it with individual implementations. I think you're stuck with STI and a bunch of unused columns across different implementations, or rethinking the services relationship in general. The delegation solution you have might work as a separate ActiveRecord, but I don't see how it works as abstract if it has to have a has_one relationship.
EDIT: So instead of your original abstract solution, why not persist the delgates? You'd have to have separate tables for MailServiceDelegate and BackupServiceDelegate -- not sure how to get around that if you want to avoid all the null columns with pure STI. You can use a module with the delgate classes to capture the common relationships and validations, etc. Sorry it took me a couple of passes to catch up with your problem:
class User < ActiveRecord::Base
has_many :services
end
class Service < ActiveRecord::Base
validates_presence_of :user_id
belongs_to :user
belongs_to :service_delegate, :polymorphic => true
delegate :common_service_method, :name, :to => :service_delegate
end
class MailServiceDelegate < ActiveRecord::Base
include ServiceDelegate
def name
# implement
end
def common_service_method
# implement
end
end
class BackupServiceDelegate < ActiveRecord::Base
include ServiceDelegate
def name
# implement
end
def common_service_method
# implement
end
end
module ServiceDelegate
def self.included(base)
base.has_one :service, :as => service_delegate
end
def name
raise "Not Implemented"
end
def common_service_method
raise "Not Implemented"
end
end
I think following will work
in user.rb
has_many :mail_service, :class_name => 'Service'
has_many :backup_service, :class_name => 'Service'
in service.rb
belongs_to :mail_user, :class_name => 'User', :foreign_key => 'user_id', :conditions=> is_mail=true
belongs_to :backup_user, :class_name => 'User', :foreign_key => 'user_id', :conditions=> is_mail=false
I've got the following models:
project.rb
has_many :tasks
task.rb
belongs_to :project
has_many :assignments
has_many :users, :through => :assignments
user.rb
has_many :assignments
has_many :tasks, :through => :assignments
assignment.rb
belongs_to :task
belongs_to :user
So for example:
Project.first.title #=> "Manhattan"
Project.first.tasks.map(&:name) # => ['Find Scientists', 'Find Money', 'Find Location']
Project.first.tasks.first.users.map(&:full_name) #=> ['James Maxwell', 'Evariste Galois', 'Jules Verne']
My first question is:
How can I find all the persons' names possibly with symbol to proc in one shot, I tried:
Project.first.tasks.users.full_name #=> AND FAILED
Project.first.tasks.map(&:users).full_name #=> AND FAILED
Project.first.tasks.map(&:users).map(&:full_name) #=> AND FAILED
Any ideas?
And I think this following question might be in the same ball park:
How can I do a find of Project with conditions that search the 'full_name' attribute of the users its tasks?
Example
Project.all(:include => {:tasks => :users}, :conditions => ['tasks.users.full_name LIKE ?', query]) #this failed
I think the problem is at the 'tasks.users'.
Thanks everyone, have a happy thanksgiving!
For the first one you'll want to do something like this:
Project.first.tasks.map { |t| t.users.map(&:full_name) }.flatten
The reason for this is that you want to iterate through all the tasks, then all the users in each task. Without the flatten this would give you a 2-dimensional array.
And for the second one your find should be:
Project.all(:include => {:tasks => :users}, :conditions => ['users.full_name LIKE ?', query])
Writing users.full_name implies to the SQL engine that you're looking for the full_name field on the users table.
I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:
Words
----
id
Links
-----
id
word1_id
word2_id
How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:
class Word < ActiveRecord::Base
has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
end
Link model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => 'Word'
# Creates the complementary link automatically - this means all synonymous
# relationships are represented in #word.synonyms
def after_save_on_create
if find_complement.nil?
Link.new(:word => synonym, :synonym => word).save
end
end
# Deletes the complementary link automatically.
def after_destroy
if complement = find_complement
complement.destroy
end
end
protected
def find_complement
Link.find(:first, :conditions =>
["word_id = ? and synonym_id = ?", synonym.id, word.id])
end
end
Tables:
Words
----
id
Links
-----
id
word_id
synonym_id
Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.
Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table
class Word < ActiveRecord::Base
has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
has_many :synonyms1, :through => :links1, :source => :word
has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
has_many :synonyms2, :through => :links2, :source => :word
end
That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.
def synonyms
return synonyms1 || synonyms2
end
||ing the results together will join the arrays and eliminate duplicates between them.
*This code is untested.
Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
def link_to(word)
synonyms << word
word.synonyms << self
end
end
Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.
Link Model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => "Word"
end
Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.
Word table:
name
Link table:
word_id
synonym_id
To link an existing word as a synonym to another word:
word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))
To create a new word as a synonym to another word:
word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:
class Concept < ActiveRecord::Base
has_many :words
end
class Word < ActiveRecord::Base
belongs_to :concept
validates_presence_of :text
validates_uniqueness_of :text, :scope => :concept_id
# A sophisticated association would be better than this.
def synonyms
concept.words - [self]
end
end
Now you can do
word = Word.find_by_text("epiphany")
word.synonyms
Trying to implement Sarah's solution I came across 2 issues:
Firstly, the solution doesn't work when wanting to assign synonyms by doing
word.synonyms << s1 or word.synonyms = [s1,s2]
Also deleting synonyms indirectly doesn't work properly. This is because Rails doesn't trigger the after_save_on_create and after_destroy callbacks when it automatically creates or deletes the Link records. At least not in Rails 2.3.5 where I tried it on.
This can be fixed by using :after_add and :after_remove callbacks in the Word model:
has_many :synonyms, :through => :links,
:after_add => :after_add_synonym,
:after_remove => :after_remove_synonym
Where the callbacks are Sarah's methods, slightly adjusted:
def after_add_synonym synonym
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end
def after_remove_synonym synonym
if complement = find_synonym_complement(synonym)
complement.destroy
end
end
protected
def find_synonym_complement synonym
Link.find(:first, :conditions => ["word_id = ? and synonym_id = ?", synonym.id, self.id])
end
The second issue of Sarah's solution is that synonyms that other words already have when linked together with a new word are not added to the new word and vice versa.
Here is a small modification that fixes this problem and ensures that all synonyms of a group are always linked to all other synonyms in that group:
def after_add_synonym synonym
for other_synonym in self.synonyms
synonym.synonyms << other_synonym if other_synonym != synonym and !synonym.synonyms.include?(other_synonym)
end
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end