How to model relationships between people?

Hi.

I’d appreciate advice and feedback, as I’m new to rails (and Ruby and mostly just a junior): am I missing something or doing it right? Are there better ways? I also have a few specific questions down the line (near the code).

I have a person and a relationship model. The idea is that I can create any kind of relationship between two people, i.e. they’re neighbors, significant others, colleagues, etc.

Some kinds of relationships such as friends, parents, and such are known and are accessible through Person, others are simply accessible via Person.relationships.

I’m using postgres as database.

The migrations.

create_people.rb

class CreatePeople < ActiveRecord::Migration[6.1]
  def change
    create_table :people do |t|
      t.string :name
      t.timestamps
    end
  end
end

(this model hasn’t been fleshed out fully, since my first objective was to make the relationships work.)

create_relationships.rb

class CreateRelationships < ActiveRecord::Migration[6.1]
  def change
    create_table :relationships do |t|
      t.integer :person_id # Person
      t.integer :with_id   # Person
      t.string :kind

      t.timestamps
    end

    add_index :relationships, %i[person_id kind]
    add_index :relationships, %i[person_id with_id kind], unique: true
  end
end
  1. naming advice: with_id – is there a better or railsy way of naming the other person?
  2. should I use t.reference for :person_id and :with_id?
  3. are the indexes proper, or should I do something differently?

The models.

person.rb

# frozen_string_literal: true

class Person < ApplicationRecord
  has_many :relationships
  has_many :friends, -> { where("relationships.kind": 'friend') }, through: :relationships, source: 'with'
  has_many :neighbors, -> { where("relationships.kind": 'neighbor') }, through: :relationships, source: 'with'
end

User specified kinds of relationships, i.e. not per se application specific are accessed via person.relationships. The relations friends and neighbors were examples to test with. Known relationships will be friends, significant_others, parents, children, etc. but the user may wish to specify their landlord.

  1. I considered splitting up kind into 2 fields: kind and inverse_kind so I could easily map parent/child relationships and allow end users to specify their custom mappings such as landlord/tenant. For data such as friends/neighbors, the inverse would just be the same.

    I figured it would save the extra row for the inverse relationship and instead just populate another column. Any thoughts? I’m concerned it may make querying the data more difficult.

  2. Talking about querying data. Is the way I’m using scopes recommended? The scope does also work on inserting data, i.e. “some_person.friends << some_other_person” executes:

# some_person.friends << some_other_person
INSERT INTO "relationships" 
    ("person_id", "with_id", "kind", "created_at", "updated_at") 
VALUES
    ($1, $2, $3, $4, $5)
RETURNING
    "id"  [
        ["person_id", 1],
        ["with_id", 2],
        ["kind", "friend"],
        ["created_at", "2021-08-09 00:33:37.507950"],
        ["updated_at", "2021-08-09 00:33:37.507950"]
    ]
  1. If not splitting up kind into 2 columns (as specified in the first item in this list) what would be the recommended way of creating inverse relationship behavior for known kinds?

    For instance, parent.children << child should then also do child.parents << parent This can be achieved by defining a method on Person such as: add_child but I’m curious if there isn’t a better way. I was wondering if there was a way I could modify the behavior to run a single transaction to do both actions for the child and the parent. I haven’t found a proper way, a single transaction would mean, if one fails: both fail.

    I looked at model hooks, but I’ve been a bit confused on how I could actually ‘hook’ into the transaction itself.

  2. Is there a way I can create custom validations for known relationships types/kinds? Such as that a parent should be older than a child. Or should I just use methods such as: add_child/parent and do it in there?

  3. I want to prevent my models from being infested with all sorts of relationship type specific code, so I’m wondering if the best way is to create specific models / relationships for known kinds that have specific requirements/validations (child/parent) - but - I also want to keep things as DRY as possible.

relationship.rb

# frozen_string_literal: true

class Relationship < ApplicationRecord
  belongs_to :person
  belongs_to :with, class_name: 'Person'
end
  1. Thoughts?

Thank you, I look forward to hearing your thoughts/suggestions/answers/anything really.

– Xander.

I’ve done something like this once. And yours may be a bit more complicated. Having both reciprocal and one way relationships is one you’ve identified. What if more than one relationship occurs: landlord and significant other, e.g.? I’m inclined to think that additional fields to start are better. Abandoning one later is probably easier than splitting a field. Rails or Postgres scripts can probably address adding the “abandoned” field information.

Your approach using with is new to me. I find it easier to think as the Relationship model as being central. Yes, it’s about people, but the everything goes through Relationships. And your People model becomes simpler. But now I see the Relationship model is more complicated.

Will it be confusing to have :person_id and :with_id for reciprocal relationships?

Are you going to have data entry in the app? Depending on how the data is received that can be complicated. I’m too amateur to figure out a good method, so I may be adding data via the Relationship model and find out one of the people isn’t in the database yet. Which if you’re not the data entry person can be complicated. I end up going out and creating the person and coming back to the relationship and somewhat starting over. Of course, you can create the person in the relationship new form but that’s another complication.

Are people going to have other fields such as addresses and will that be another model?

If you have an age check for parents and children, what if ages aren’t known. Do you want to use approximate ages? In other words to keep things moving.

As I said at the outset, I don’t know much. But the usual advice is to not get hung up on the edge cases at first and then build out the app. Good luck.

When you talk about relationships in general, there are a few types

  • Unidirectional (eg. parent/child)
  • Bidirectional (eg. friend/friend)
  • Group (eg. football team member)

I think unidirectional is a straightforward 1:1 mapping. Group would be a 1:n mapping. Bidirectional is the tricky one - you can implement it as two unidirectional mappings, or as a type of group mapping.