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
|