Recipe 3.22. Mixing Join Models and Polymorphism for Flexible Data Modeling


Recipe 3.22. 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

  • Section 3.21"




Rails Cookbook
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net