Search code examples
ruby-on-railsassociationsmodel-associationsbelongs-to

belongs_to belongs_to association only no has_many or has_one


Can you have a belongs_to belongs_to relationship in Rails?

Search results gave me two results. The fact that I can't find very much info on the subject, seems to indicate that it shouldn't be done or is bad practice.

I asked yesterday about a has_many relationship, but thought because I couldn't find information on this, I would generate a question so it is easier for people to search for this in the future. I'm paraphrasing another users answer (I hope this is ok).

A Shoe can have many Socks, but only one active Sock. Other Socks are inactive. All Socks are also unique with unique patterns. I want to make sure I don't have socks with the same pattern. I think I can do this three ways

class Sock < ActiveRecord::Base
  belongs_to :shoe
end

To find out if a Sock is active or inactive, give its' owner shoe a reference to its active sock like so:

class Shoe < ActiveRecord::Base
  belongs_to :sock
end

Go to its owner Shoe and check if the Shoe's active sock is the current Sock or not. E.g.

def is_active
  owner_shoe.active_sock == self

Associate them with foreign keys

class CreateGettingDressed < ActiveRecord::Migration
  def change
    create_table :shoes do |t|
      t.belongs_to :active_sock, foreign_key: "sock_id"
      t.string :size
      t.timestamps null: false
    end

    create_table :socks do |t|
      t.belongs :owner_shoe, foreign_key: "shoe_id"
      t.string :pattern
    end
  end
end

Solution

  • The problem you have is your two pieces of functionality are conflicting:

    A Shoe can have many Socks, but only one active Sock

    You're looking to associate the two models on two different associations. Although this is simply done, I feel the way you're trying to do is is a little restricted.

    Here's how I'd set up the base association:

    #app/models/sock.rb
    class Sock < ActiveRecord::Base
       #columns id | shoe_id | name | active (boolean) | created_at | updated_at
       belongs_to :shoe
    end
    
    #app/models/shoe.rb
    class Shoe < ActiveRecord::Base
       #columns id | name | etc | created_at | updated_at
       has_many :socks
       scope :active, -> { where(active: true).first }
    end
    

    This will give you the ability to call:

    @shoe = Shoe.find 1
    @shoe.socks.active #-> first sock with "active" boolean as true
    

    It will also negate the need to include an active? method in your sock model. You can call @shoe.socks.find(2).active? to get a response as to whether it's active or not.


    Now, this should work pretty well for basic functionality.

    However, you mention several extensions:

    if a Sock is active or inactive

    I want to make sure I don't have socks with the same pattern

    This adds extra specifications which I'd tackle with a join model (has_many :through):

    #app/models/sock.rb
    class Sock < ActiveRecord::Base
       has_many :shoe_socks
       has_many :shoes, through: :shoe_socks
    end
    
    #app/models/shoe_sock.rb
    class ShoeSock < ActiveRecord::Base
       # columns id | shoe_id | sock_id | pattern_id | active | created_at | updated_at
       belongs_to :shoe
       belongs_to :sock
       belongs_to :pattern
    end
    
    #app/models/shoe.rb
    class Shoe < ActiveRecord::Base
       has_many :shoe_socks
       has_many :socks, through: :shoe_socks, extend: ActiveSock
       scope :active, -> { where(active: true).first }
    end
    

    You can read more about the below code here:

    #app/models/concerns/active_sock.rb
    module ActiveSock
    
        #Load
        def load
          captions.each do |caption|
              proxy_association.target << active
          end
        end
    
        #Private
        private
    
        #Captions
        def captions
                return_array = []
                through_collection.each_with_index do |through,i|
                        associate = through.send(reflection_name)
                        associate.assign_attributes({active: items[i]})
                        return_array.concat Array.new(1).fill( associate )
                end
                return_array
        end
    
        #######################
        #      Variables      #
        #######################
    
        #Association
        def reflection_name
                proxy_association.source_reflection.name
        end
    
        #Foreign Key
        def through_source_key
                proxy_association.reflection.source_reflection.foreign_key
        end
    
        #Primary Key
        def through_primary_key
                  proxy_association.reflection.through_reflection.active_record_primary_key
        end
    
        #Through Name
        def through_name
                proxy_association.reflection.through_reflection.name
        end
    
        #Through
        def through_collection
                proxy_association.owner.send through_name
        end
    
        #Captions
        def items
                through_collection.map(&:active)
        end
    
        #Target
        def target_collection
                #load_target
                proxy_association.target
        end
    

    This setup will basically put all the "logic" into the join model. IE you'll have a database of socks, one of shoes and a connecting DB with parings of both.

    This will still permit you to call @shoe.socks.active but without having to degrade the data integrity in your data models.

    I have also added some code I wrote a while back - which gives you the ability to access attributes from the join model. It uses the proxy_association object in ActiveRecord, so it doesn't invoke any more SQL.

    This added code will append the active? attribute to any associative Sock objects.