Search code examples
ruby-on-railsrelationshipmodels

About the rails way relation models


Context:

I have two tables, challenges and challenge_steps. Both tables need to have relation between them, I need to be able to reference a Step with a Challenge and the inverse relationship.

A challenge can have multiple steps but ONLY ONE current_step.

Schema:

Challenge:

t.string   "name"
t.string   "subtitle"
t.text     "brief",                     null: false
t.integer  "min_team_size", default: 2, null: false
t.integer  "max_team_size", default: 5, null: false
t.datetime "created_at",                null: false
t.datetime "updated_at",                null: false

Challenge::Step:

t.integer  "challenge_id"
t.string   "name"
t.text     "description"
t.datetime "start_at"
t.datetime "end_at"
t.datetime "created_at",   null: false
t.datetime "updated_at",   null: false

To do this I can think of three solutions, but none of them are satisfying:

Solution One:

Challenge Model:

  has_many :steps, inverse_of: :challenge, dependent: :destroy
  belongs_to :current_step, class_name: Challenge::Step

Challenge::Step:

  belongs_to :challenge
  has_one :challenge_relation, class_name: Challenge,
                               foreign_key: :current_step_id, dependent: :restrict_with_error

As you can see in my Challenge::Step model I have a belongs_to(:challenge) and the Rails documentation reads:

For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier.

So the behavior is OK, but the code looks odd.

Solution Two:

Create a table which contains challenge_id and step_id. Which will reference each challenge and its current_step

This one is good but it mean we need the read another table to get the needed info.

Solution Three:

add in the Challenge model:

has_many :steps, inverse_of: :challenge, dependent: :destroy do
    def current_step
      proxy_association.owner.steps.where(current_step: true).first
    end
end

It returns a collection and the schema doesn't respect the real relation between a Challenge and his step.

What would most efficient and elegant? Could you think of a solution which would have none of these drawbacks ?


Solution

  • First of all, why is Challenge::Step a subclass of Challenge?

    Surely you'd want it to be Step on its own? For the purposes of clarity, I will refer to it just as Step.

    --

    Here's what I'd do:

    #app/models/challenge.rb
    class Challenge < ActiveRecord::Base
        has_many :steps
        def current
            steps.where(current: true).order(current: :desc).first 
        end
    end
    
    #app/models/step.rb
    class Step < ActiveRecord::Base
       # columns id | challenge_id | current (datetime) | etc...
       belongs_to :challenge
    end
    

    This will give you the ability to call:

    @challenge = Challenge.find params[:id]
    # @challenge.steps = collection of steps
    # @challenge.current_step = latest current step
    

    The idea being that you could save your current_step attribute as a date in the Step model. This will have the added benefit of giving you the ability to see the historical record of when each step was "current".

    --

    An alternative would be to make a current column in the Challenge model:

    #app/models/challenge.rb
    class Challenge < ActiveRecord::Base
       # columns id | name | current | etc
       has_many :steps
       def current_step
          steps.find current
       end
    end
    
    #app/models/step.rb
    class Step < ActiveRecord::Base
       #columns id | challenge_id | name | etc
       belongs_to :challenge
    end
    

    This will allow you to call the following:

    @challenge = Challenge.find params[:id]
    # @challenge.steps = collection of steps
    # @challenge.current_step = single instance of step
    

    --

    Your third solution is by far most elegant, but it assumes the structure you have implemented being correct.

    I think you don't have the correct setup to handle the current_step attribute; you either need a way to distinguish it in the Step model or the Challenge model.