'I am stumped by a seemingly simple active_record association

I have 2 tables. One contains genealogy data (families), the other contains people data. I want to create an association so that I can access the people data based on pointers from the genealogy table. For instance:

    <% @families.each do |family| %>
...
    <td> <%= family.father.first_name %></td>

Here is the conroller code:

  def index
      @families = Family.joins(:people)
  end

Here are the 2 schemas:

CREATE TABLE "people" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name_prefix" varchar, "first_name" varchar, "middle_name" varchar, "last_name" varchar, "name_suffix" varchar, "date_of_birth" varchar, "date_of_death" varchar, "gender" varchar, "notes" varchar, "sync_outlook" varchar, "sync_phone" varchar, "flags" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "place_of_birth" varchar, "aliases" varchar);

CREATE TABLE "families" ("id" integer NOT NULL PRIMARY KEY, "fam_notes" varchar DEFAULT NULL, "fam_fatherid" integer DEFAULT NULL, "fam_motherid" integer DEFAULT NULL, "fam_weddingdate" varchar DEFAULT NULL, "fam_weddingplace" varchar DEFAULT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

Here is the model code:

class Family < ApplicationRecord
    has_many :childlinks, foreign_key: "child_family"
    has_many :people, through: :childlinks

    #has_one :father, through: :families, source: "fam_fatherid"
    has_one :father, foreign_key: "fam_fatherid"
    has_one :person, through: :father
end

When I put a break in the web page, @families exists, but the associated father does not.

>> @families[0].father
NameError: uninitialized constant Family::Father

I have tried every combination of has_one, through, foreign_key, source, etc. that I can think of, and nothing has produced a 'family' object with a linked (or associated) 'father' object.

What am I doing wrong?



Solution 1:[1]

I'm going to assume here that we are just modeling biological assocations and not the whole can of worms that is the real world and I'm going to stick to the Rails conventions instead of unfuddling your schema.

If you want to put the mother/father combo as different columns on one table this is how you would do it:

class CreateFamilies < ActiveRecord::Migration[6.1]
  def change
    create_table :families do |t|
      t.belongs_to :father, 
        null: false, 
        foreign_key: { to_table: :persons }
      t.belongs_to :mother, 
        null: false, 
        foreign_key: { to_table: :persons }
      t.timestamps
    end
  end
end
class Family < ApplicationRecord
  belongs_to :father, 
    class_name: 'Person'
  belongs_to :mother, 
    class_name: 'Person'
  has_many :children, 
    class_name: 'Person',
    inverse_of: :family
end
class Person < ApplicationRecord 
  # @todo add a family_id foreign key column to persons
  # this is a persons link to their parents
  # Must be nullable to avoid a chicken-vs-egg scenario
  belongs_to :family, 
    optional: true,
    inverse_of: :children
  has_one :father, through: :family
  has_one :mother, through: :family
end

Note that you don't actually need a join table between children and the family as the relationshop is one to many and not many to many. Each child can only belong to one set of parents. You also need to use belongs_to when the foreign key is on this models table.

So far its pretty simple. However once you start adding relations to a persons children it gets crazy:

class Person < ApplicationRecord 
  # this is a persons link to their parents
  # Must be nullable to avoid a chicken-vs-egg scenario
  belongs_to :family, 
    optional: true,
    inverse_of: :children

  has_one :father, through: :family
  has_one :mother, through: :family

  has_many :maternal_families,
    class_name: 'Family',
    foreign_key: :mother_id

  has_many :maternal_children,
    through: :maternal_families,
    source: :children

  has_many :paternal_families,
    class_name: 'Family',
    foreign_key: :father_id

  has_many :paternal_children,
    through: :paternal_families,
    source: :children
end

The reason you need so many assocations is that each has_many assocation must point to a single foreign key in ActiveRecord.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1