Recipe 3.22. Mixing Join Models and Polymorphism for Flexible Data ModelingProblemContributed 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. SolutionFor 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 ... DiscussionIn 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
|