How to model data in Rails?

After reading some books and working on some tutorials, I’m working on my first Rails app. I’ve run into a question about some of my models.

In the app, I want to allow an admin user to create a pool of chores. At different intervals, the admin user can assign each chore to 0-N other users to be completed by them (e.g. Fred must do “Brush teeth” and “Make bed”, and Wilma also does “Brush teeth”). When the users complete their assigned chores, they receive points in recognition of their completing the chore. Those points have a default value in the pool but can be overwritten by the admin when assigning the chore to a particular user (e.g. Fred and Barney are both assigned “Set the dinner table”, Fred gets the default value for it when he does it, but Barney gets default + 10 when he does it).

Initially, I was putting everything that could be true of a chore on the Chore model (e.g. name, earnings, start, due, user_id, etc). I realized, however, some of those properties were not true of a Chore as it exists in the pool but were really true of the assignment of a chore. For example, start and due are not true of the chore as a Chore. start and due are true of a chore when it is assigned to a user. So I’m introducing a ChoreAssignment model that HAS_A Chore.

Here are some questions I’m running into:

First, am I thinking of this correctly that I need two models, one for the defaults that are part of the pool and one for an actual instance of doing the chore?

Second, if the ChoreAssignment HAS_A Chore (in the OO sense, not necessarily in the ActiveRecord sense) and the ChoreAssignment can override the default values of some of the Chore attributes when it is assigned, should the ChoreAssignment have the same attributes (db columns) as a Chore and it takes those from the Chore if it hasn’t been overwritten? Or is there some other way to model that?

Here are some example classes of what I’m thinking about in just Ruby not ActiveRecord:

class Chore
  attr_reader :name, :earnings, :expires
  def initialize(name:, earnings: 0, expires: false)
    @name = name
    @earnings = earnings
    @expires = expires
  end
end
# When this is saved to the db, it would get non-overwritable
# attributes from the chore (e.g. name).
class ChoreAssignment
  attr_reader :chore, :user_id, :start, :due
  def initialize(chore:, user_id:, start: nil, due: nil, earnings:, expires:)
    @chore = chore
    @user_id = user_id
    @start = start
    @due = due
    @earnings = earnings || chore.earnings
    @expires = expires || chore.expires
  end
end

If I’m thinking of this correctly, how do I set up the ChoreAssignment in ActiveRecord?

Thanks for any help you can offer.

Second, if the ChoreAssignment HAS_A Chore (in the OO sense, not necessarily in the ActiveRecord sense) and the ChoreAssignment can override the default values of some of the Chore attributes when it is assigned, should the ChoreAssignment have the same attributes (db columns) as a Chore and it takes those from the Chore if it hasn’t been overwritten? Or is there some other way to model that?

Lookup STI or Single Table Inheritance in Rails. That is pattern to implement what you are asking in the last question there.

As for the rest, I think you are doing it sensibly enough. :slight_smile:

I would advise against using STI here unless there’s a small and known set of values you’ll be overriding with.

This “pattern” might be more known to model Plan + Subscription, where Chore maps to Plan, and ChoreAssignment maps to Subscription. The way I’ve done it in the past is exactly how you proposed, Lance: add the overridable columns to ChoreAssignment, and always ask ChoreAssignment for the values, which asks them from Chore if they weren’t overridden.

The idea for Plan+Subscription is that sporadically you might want to give certain customers (e.g. friends) unique discounts or limits such as set the price to 0, or double the space, and you don’t want to mess around with more classes to generalise that.

Thanks for your suggestion. I’ve read up on Single Table Inheritance. Through the reading though I’ve come up with a different idea that I’ll add in a separate reply.

Thanks @dv1 and @CDainMiller for your suggestions. Looking at it again this morning I’m thinking about separating out another model. Chore will be those attributes that cannot be changed. ChoreImplication are those values that can be set by default when the Chore is made in the pool but overwritten during an assignment. ChoreAssignment will have both a Chore and a ChoreImplication. If the implication values are not overwritten it will just point to the same ChoreImplication as its Chore. If they are, it will create a ChoreImplication with the values it needs for the overwrite.

The classes will look something like:

class Chore < ActiveRecord::Base
  has_one :chore_implication
  attr_reader :name
end

class ChoreImplication < ActiveRecord::Base
  # Attributes that can be set as defaults on a
  # Chore but can be overridden during the
  # assignment of a Chore.
  attr_reader :earnings, :expires
end

class ChoreAssignment < ActiveRecord::Base
  has_one :chore
  has_one :chore_implication
  has_one :worker

  def initialize(chore:, implication: nil, start:, due:)
    @chore = chore
    @implication = implication || chore.implication
  end
end

I think this prefers Composition over Inheritance and should get me what I’m after.

Any thoughts?