Search code examples
ruby-on-railsrubymodelsrelationships

Linking many existing models to one new one. Ruby on Rails


So I am making an app that reviews books, articles and the like.

I have created the backbone of the app by creating models, views, controllers etc for Piece(the book or article), Section(self explanatory), Subsection, and Subsubsection.

I want to add a new model into the mix, a "Links" model (which will just be a link to another source or website). My issue is that I don't know how to make ALL of my previously stated models have "Links". I want each of The above models to have access and CRUD capabilities to their "Links", but so far all i have read about is has_many or has_and_belongs_to_many.

As far as I understand, those kinds of relations only relate ONE model to ONE other model, even if Piece might have many Sections, it only relates these two models.

I guess the Links model would have to have an obligatory piece_id, but then optional id's such as: section_id, subsection_id depending on where the link was. So if in Chapter 3 of my first book i want to add a link, it would have an obligatory piece_id=1 and then a section_id=3, but then no subsection_id or subsubsection_id.

So how do I go about creating a model such that it belongs to several other models? Or is this even possible?

https://github.com/kingdavidek/StuddyBuddy


Solution

  • This is a pretty good use case for polymorphic associations. For simplicity lets start out with a one to many relationship:

    class Link < ActiveRecord::Base
      belongs_to :linkable, polymorphic: true
    end
    
    class Piece < ActiveRecord::Base
      has_many :links, as: :linkable
    end
    
    class Section < ActiveRecord::Base
      has_many :links, as: :linkable
    end
    

    Here the links table will have linkable_id (int) and linkable_type (string) columns. One important thing to take note of here is that linkable_id is not a true foreign key from the RBDMS point of view. Rather ActiveRecord resolves which table the relation points to when it loads the relation.

    If we want to cut the duplication we can create a module which contains the desired behavior. Using ActiveSupport::Concern cuts a lot of the boilerplate code involved in creating such a module.

    class Link < ActiveRecord::Base
      belongs_to :linkable, polymorphic: true
    end
    
    # app/models/concerns/linkable.rb
    module Linkable
      extend ActiveSupport::Concern
    
      included do
        has_many :links, as: :linkable
      end
    end
    
    class Piece < ActiveRecord::Base
      include Linkable
    end
    
    class Section < ActiveRecord::Base
      include Linkable
    end
    

    So how would we make a polymorpic many to many relation?

    class Link < ActiveRecord::Base
      has_many :linkings
    end
    
    # this is the join model which contains the associations between
    # Links and the "linkable" models 
    class Linking < ActiveRecord::Base
      belongs_to :link
      belongs_to :linkable, polymorphic: true
    end
    
    # app/models/concerns/linkable.rb
    module Linkable
      extend ActiveSupport::Concern
    
      included do
        has_many :links, through: :linkings, as: :linkable
      end
    end
    
    class Piece < ActiveRecord::Base
      include Linkable
    end
    
    class Section < ActiveRecord::Base
      include Linkable
    end
    

    On a side note - a better way to build a hierarchy between sections would be to use a single Section model and give it a self joining relationship.

    class Section < ActiveRecord::Base
      belongs_to :parent, class_name: 'Section'
      has_many :children, class_name: 'Section', 
                          foreign_key: 'parent_id'
    end