Mixing Join Models and Polymorphism for Flexible Data Modeling
Problem
Contributed by: Diego Scataglini
 Your application   contains models in a many-to-many relationship. The relationship exhibits important characteristics that merit the creation of a full-fledged model to describe them. For example, you want to model a reader's subscription to one or more entities such as newspaper, magazine, or blog.
 Solution
For this recipe, create a Rails project called polymorphic:
 
$ rails polymorphic
 
 
From the root directory of the application, generate the following models:
 
$ ruby script/generate model Reader
 exists app/models/
... create db/migrate/001_create_readers.rb
$ ruby script/generate model Subscription
... create db/migrate/002_create_subscriptions.rb
$ ruby script/generate model Newspaper
... create db/migrate/003_create_newspapers.rb
$ ruby script/generate model Magazine
... create db/migrate/004_create_magazines.rb
 
 
Now, add table definitions for each of the migrations created by the generator:
 db/migrate/001_create_readers.rb:
 
class CreateReaders < ActiveRecord::Migration
 def self.up
 create_table :readers do |t|
 t.column :full_name, :string
 end
 Reader.create(:full_name => "John Smith")
 Reader.create(:full_name => "Jane Doe")
 end
 def self.down
 drop_table :readers
 end end
 
 
db/migrate/002_create_subscriptions.rb:
 
class CreateSubscriptions < ActiveRecord::Migration
 def self.up
 create_table :subscriptions do |t|
 t.column :subscribable_id, :integer
 t.column :subscribable_type, :string
 t.column :reader_id, :integer
 t.column :subscription_type, :string
 t.column :cancellation_date, :date
 t.column :created_on, :date
 end
 end
 def self.down
 drop_table :subscriptions
 end end
 
 
db/migrate/003_create_newspapers.rb:
 
class CreateNewspapers < ActiveRecord::Migration
 def self.up
 create_table :newspapers do |t|
 t.column :name, :string
 end
 Newspaper.create(:name => "Rails Times")
 Newspaper.create(:name => "Rubymania")
 end
 def self.down
 drop_table :newspapers
 end end
 
 
db/migrate/004_create_magazines.rb:
 
class CreateMagazines < ActiveRecord::Migration
 def self.up
 create_table :magazines do |t|
 t.column :name, :string
 end
 Magazine.create(:name => "Script-generate")
 Magazine.create(:name => "Gem-Update")
 end
 def self.down
 drop_table :magazines
 end end
 
 
Ensure your database.yml file is configured to access your database, and migrate your database schema:
 
$ rake db:migrate
 
 
Define your Subscription model as a polymorphic model, and specify its relationship to Reader:
 app/models/subscription.rb:
 
class Subscription < ActiveRecord::Base
 belongs_to :reader
 belongs_to :subscribable, :polymorphic => true end
 
 
Now reciprocate the relationship from the Reader side.
 app/models/reader.rb:
 
class Reader < ActiveRecord::Base
 has_many :subscriptions end
 
 
Next, define your Magazine and Newspaper classes to have many subscriptions and subscribers:
 app/models/magazine.rb:
 
class Magazine < ActiveRecord::Base
 has_many :subscriptions, :as => :subscribable
 has_many :readers, :through => :subscriptions end
 
 
app/models/newspaper.rb:
 
class Newspaper < ActiveRecord::Base
 has_many :subscriptions, :as => :subscribable
 has_many :readers, :through => :subscriptions end
 
 
Now update Subscription and Reader classes as follows:
 app/model/subscription.rb:
 
class Subscription < ActiveRecord::Base
 belongs_to :reader
 belongs_to :subscribable, :polymorphic => true
 belongs_to :magazine, :class_name => "Magazine",
 :foreign_key => "subscribable_id"
 belongs_to :newspaper, :class_name => "Newspaper",
 :foreign_key => "subscribable_id"
end
 
 
app/models/reader.rb:
 
class Reader < ActiveRecord::Base
 has_many :subscriptions
 has_many :magazine_subscriptions, :through => :subscriptions, 
 :source => :magazine, 
 :conditions => "subscriptions.subscribable_type = 'Magazine'"
 has_many :newspaper_subscriptions, :through => :subscriptions, 
 :source => :newspaper, 
 :conditions => "subscriptions.subscribable_type = 'Newspaper'"
end
 
 
You now have a bidirectional relationships between your Reader model and the periodicals Newspaper and Magazine.
 
>> reader = Reader.find(1)
>> newspaper = Newspaper.find(1)
>> magazine = Magazine.find(1)
>> Subscription.create(:subscribable => newspaper, :reader => reader,
 :subscription_type => "Monthly")
>> Subscription.create(:subscribable => magazine, :reader => reader,
 
 :subscription_type => "Weekly")
 
)
>> reader.newspaper_subscriptions
=> [#<Newspaper:0x36c1008 @attributes={"name"=>"Rails Times", 
"id"=>"1"}>]
>> reader.magazine_subscriptions
=> [#<Magazine:0x36bca30 @attributes={"name"=>"Script-generate", 
"id"=>"1"}>]
>> newspaper.readers
=> [#<Reader:0x36a3314 ...
>> magazine.readers
=> [#<Reader:0x36a3314 ... 
Discussion
 In this example, you created relationships between the polymorphic
 models 
 Magazine
 and Newspaper, and Reader. Polymorphic associations through a
 full-fledged model can be tricky to set up correctly but can help to
 model your domain more accurately. The key to specifying the
 relationship between Reader and
 Magazine was to use the :source option to identify the Magazine class, and the :tHRough option to specify that a Subscription links a
 Reader to a Magazine. Spend some time studying the
 previous code, and be sure to use the console to explore the model
 objects. The combined power of has_many
 :through and polymorphic associations provides you with a slew
 of dynamic methods to experiment with. The easiest way to figure out
 what methods are available is to grep
 them. First, open a Rails console:
 
$ ruby script/console
 
 Then enter the following command to view the dynamic
 methods:
 
>> puts reader.methods.grep(/subscri/).sort
add_magazine_subscriptions add_newspaper_subscriptions add_subscriptions build_to_magazine_subscriptions build_to_newspaper_subscriptions build_to_subscriptions create_in_magazine_subscriptions create_in_newspaper_subscriptions create_in_subscriptions find_all_in_magazine_subscriptions find_all_in_newspaper_subscriptions find_all_in_subscriptions find_in_magazine_subscriptions find_in_newspaper_subscriptions find_in_subscriptions has_magazine_subscriptions?
has_newspaper_subscriptions?
has_subscriptions?
remove_magazine_subscriptions remove_newspaper_subscriptions remove_subscriptions magazine_subscriptions magazine_subscriptions_count newspaper_subscriptions newspaper_subscriptions_count subscription_ids=
subscriptions subscriptions=
subscriptions_count validate_associated_records_for_subscriptions
 
  
>> puts magazine.methods.grep(/(reade|subscri)/).sort
add_readers add_subscriptions build_to_readers build_to_subscriptions create_in_readers create_in_subscriptions find_all_in_readers find_all_in_subscriptions find_in_readers find_in_subscriptions generate_read_methods generate_read_methods=
has_readers?
has_subscriptions?
readers readers_count remove_readers remove_subscriptions subscription_ids=
subscriptions subscriptions=
subscriptions_count validate_associated_records_for_subscriptions
 
 Because you used a join model for your many-to-many relationship
 setup, you can easily add both data and behavior to the
 subscriptions. If you look back at
 db/migrate/002_create_subscriptions.rb, you'll see
 that you gave the subscription model attributes of its own. It doesn't
 just link records to each other; it holds important information, such as
 the date the subscription was created, the date the subscription
 expires, and the type of subscription (monthly or weekly). You can refine the models even further. Say you want to give
 Magazine a method to return
 subscription cancellations: app/models/magazine.rb:
 
class Magazine < ActiveRecord::Base
 has_many :subscriptions, :as => :subscribable
 has_many :subscribers, :through => :subscriptions 
 has_many :cancellations, :as => :subscribable,
 :class_name => "Subscription" ,
 :conditions => "cancellation_date is not null"
end
 
 Test your new methods in script/console:
 
   
>> Magazine.find(:first).cancellations_count
=> 0
>> m = Magazine.find(:first).subscriptions.first
=> #<Subscription:0x32d8a18 @attributes={"cre ....
>> m.cancellation_date = Date.today
=> #<Date: 4908027/2,0,2299161>
>> m.save
=> true
>> Magazine.find(:first).cancellations_count
=> 1
 
 
See Also
 
 
 |