Search code examples
ruby-on-railsassociationsself-join

Rails: Self-referential associations have my head spinning


I'm currently working on a small school project that utilizes Ruby on Rails and I'm having some trouble getting my self-referential associations working correctly.

Context

The intended functionality of my web app is for users to post houses/apartments for other users to search through and rent. Since I'm having issues with a specific association, I'm working with a completely stripped down version that only has two models, User and Lease.

What I'm Trying to Accomplish

Ideally, when a person first registers on the site, a User object is created to hold their information such as email and password. A User can then either post a listing or search through listings.

Once a post has been created and another user decides to rent the posted house, a Lease object is created, which holds the ID of the posting User as well as the ID of the renting user, aliased as "landlord_id" and "tenant_id" respectively.

A User should now be identified as either a User, Landlord or a Tenant (or both Landlord and Tenant) based on whether there are any Lease objects with their ID as either a Landlord or a Tenant. This identification will be used to determine whether the User can access other areas of the site.

userFoo.leases

This should give me a list of all Lease objects with which the User's ID is associated, regardless of whether it's as a Landlord or Tenant.

userFoo.tenants

This should give me a list of any User object whose ID is associated with the ID of userFoo as a Tenant through Lease, and the inverse if I ask for landlords.

The Code

User Class

class User < ApplicationRecord
  has_many :tenants, class_name: "Lease", foreign_key: "landlord_id"
  has_many :landlords, class_name: "Lease", foreign_key: "tenant_id"
end

Lease Class

class Lease < ApplicationRecord
  belongs_to :landlord, class_name: "User"
  belongs_to :tenant, class_name: "User"
end

Users Table Migration

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :password_digest

      t.timestamps
    end
  end
end

Leases Table Migration

class CreateLeases < ActiveRecord::Migration[6.0]
  def change
    create_table :leases do |t|
      t.references :landlord, null: false, foreign_key: {to_table: :users}
      t.references :tenant, null: false, foreign_key: {to_table: :users}

      t.timestamps
    end
  end
end

Database Schema

ActiveRecord::Schema.define(version: 2020_10_18_005954) do

  create_table "leases", force: :cascade do |t|
    t.integer "landlord_id", null: false
    t.integer "tenant_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["landlord_id"], name: "index_leases_on_landlord_id"
    t.index ["tenant_id"], name: "index_leases_on_tenant_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.string "password_digest"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "leases", "users", column: "landlord_id"
  add_foreign_key "leases", "users", column: "tenant_id"
end

What's Wrong?

userFoo.leases

Normally a User would have_many leases by having their ID associated with a lease as "user_id." However, since I'm using "tenant_id" and "landlord_id", this command fails because it can't find "user_id" in the Leases table.

userFoo.tenants

This command gives me a list of all Lease objects where userFoo's ID is associated as "landlord_id" instead of all User objects associated with userFoo's ID as tenants. To retrieve a tenant as is, I have to use the command:

userFoo.tenants.first.tenant

Conclusion I am having a bit of a hard time understanding these deeper, more complex associations, and I've spent some time trying to find a detailed reference on has_many that covers all the arguments, but all I can really find are small blog posts that reference the "Employees" and "Managers" example on guides.rubyonrails.com . I think one problem is that I'm not sure I'm correctly reflecting my model associations in my table schema.

I'm more than happy to teach myself if someone can point me in the right direction. I'm also open to alternative solutions but only if I can't get the functionality I want out of this setup, because my instructor specifically asked me to try it this way

Thanks in advance for any help! It's much appreciated.


Solution

  • as per your requirement you can try like this:

    # app/models/user.rb
    
    class User < ApplicationRecord
      has_many :owned_properties, class_name: "Property", foreign_key: "landlord_id"
      has_many :rented_properties, class_name: "Property", foreign_key: "tenant_id"
    end
    

    Here I have declared two associations with same table but different foreign keys.

    # app/models/property.rb
    
    class Property < ApplicationRecord
      belongs_to :landlord, class_name: "User"
      belongs_to :tenant, class_name: "User"
    end
    

    Here I have taken one table by using this user can post one property where landlord is the owner of a house and later you can add tenant who is taking rent to one property.

    # db/migrations/20201018054951_create_users.rb
    
    class CreateUsers < ActiveRecord::Migration[6.0]
      def change
        create_table :users do |t|
          t.string :name,            null: false
          t.string :email,           null: false, index: true
          t.string :password_digest, null: false
    
          t.timestamps
        end
      end
    end
    

    Above is your users table migration.

    # db/migrations/20201018055351_create_properties.rb
    
    class CreateProperties < ActiveRecord::Migration[6.0]
      def change
        create_table :properties do |t|
          t.references :landlord, foreign_key: {to_table: :users}, null: false
          t.references :tenant,   foreign_key: {to_table: :users}
    
          t.timestamps
        end
      end
    end
    

    Above is your properties table migration.

    # db/schema.rb
    
    ActiveRecord::Schema.define(version: 2020_10_18_055351) do
    
      create_table "properties", force: :cascade do |t|
        t.bigint "landlord_id", null: false
        t.bigint "tenant_id"
        t.datetime "created_at", precision: 6, null: false
        t.datetime "updated_at", precision: 6, null: false
        t.index ["landlord_id"], name: "index_properties_on_landlord_id"
        t.index ["tenant_id"], name: "index_properties_on_tenant_id"
      end
    
      create_table "users", force: :cascade do |t|
        t.string "name", null: false
        t.string "email", null: false
        t.string "password_digest", null: false
        t.datetime "created_at", precision: 6, null: false
        t.datetime "updated_at", precision: 6, null: false
        t.index ["email"], name: "index_users_on_email"
      end
    
      add_foreign_key "properties", "users", column: "landlord_id"
      add_foreign_key "properties", "users", column: "tenant_id"
    end
    

    If you want to fetch all the owned properties of a user, use user.owned_properties.

    If you want to fetch all rented properties of a user, use user.rented_properties.

    ^^ Here both the cases you'll get objects of Property class.

    If you want to get landlord of a property, use property.landlord.

    If you want to get tenant of a property, use property.tenant.

    ^^ Here both the cases you'll get objects of User class.

    If you want you can add other attributes like: name, price, etc to properties table.

    I think, this will help you. Thanks :) Happy Coding :)