Search code examples
ruby-on-railssti

Rails: using STI to model client and partner records


I know STI is a debated topic within the Rails community (and probably others), which is the reason I'm trying to find a different solution to my problem before going down the STI route.

I'm building a system that has a contact management portion, which contains both client and partner records. The difference is that partners will have an associated partner_type and a few additional fields that client will not have.

This looks like a good case for STI. The records are of the same "category", meaning they all represent "people" but in different ways. They will all have the same core fields and have many email_addresses/phone_numbers.

But the biggest requirement that led me to STI instead of separate tables, is that I need to list all of the contacts together alphabetically. My employer doesn't want separate pages for client records and partner records. If I broke this into multiple tables, I would have to somehow query both tables and arrange them alphabetically, also while taking pagination into account (will have thousands of records for each type).

Is there another solution besides STI? I know many developers have run into problems with STI before, but I'm leaning towards this is a text-book case where STI may actually work.

class Contact < ApplicationRecord
  has_many :email_addresses # probably use polymorphic
  has_many :phone_numbers # probably use polymorphic 

  validates :first_name, :last_name, presence: true
end

class Client < Contact
end

class Partner < Contact
  belongs_to :partner_type

  validates :partner_type, presence: true

  # some attributes only applicable to client
  validates :client_unique_field1, :client_unique_field2, presence: true
end

Solution

  • There are two design decisions you need to make this case:

    1. Should partners and clients share the same table?

      a. If "no", then you simple create separate tables and separate models.

      b. If "yes", then you have a second design question to answer #2.

    2. Should partners and clients share the same model class?

      a. If "yes", then you can use an emum to identify the different roles of partner and client and use that enum to drive your business logic.

      b. If "no" then you should implement STI.

    I think there is a strong case to say "yes" to #1. It seems client and partner are both fundamentally the same thing. They are both people. More importantly, they will contain most of the same information so sharing a table makes good sense.

    So that leaves you with whether or not to use STI or an enum. The fundamental decision you need to make surrounds business logic associated with partners and clients.

    If most of the business logic is shared, then it makes sense to use an enum. Let me give you an example. In one of my projects, I have a User model. All users can do basic things on the site. However, we also have school_admin users and class_admin users. Admins of course have greater access to portions of the site, but from a business logic perspective, there are only a couple of relations and a couple of methods that are unique to an admin and not shared by a user.

    Since 95% of the business logic is shared between normal users and admins, I elected to keep them all in one class. I used an enum called role to distinguish users:

    # in the User model
    enum :role, [:graduate, :school_admin, :class_admin]
    

    In the users table I have a column of type int called role. The enum opens up a bunch of helper methods, such as class_admin?, to make the business logic work.

    Your case may be different. It seems clients and partners may have greater differences in business logic in your app. I don't know, but it sounds like there are some fundamental differences in their roles. You will have to decide how much business logic is shared between them and how much is different. If they are different enough, then STI makes sense.

    Furthermore, you may want to go the STI route if you would like to take advantage of inheritance in methods. For example: you may have a contact_verified? method where partner.contact_verified? has different business logic (email and phone maybe) than client.contact_verified? (email only). A weak example maybe, but you get the idea. Of course, you could accomplish the same thing with a conditional inside contact_verified? when using the single model approach.

    You are correct that the some in the Rails community tend to be down on STI. So do not make the decision to go the STI route lightly. However, I have used STI successfully in some apps with few STI-related problems.

    It all depends on how much business logic is shared and if you want to take advantage of inheritance. The decision is ultimately up to you.