Home > database >  Why isn't Ruby on Rails loading my associated objects with .includes()?
Why isn't Ruby on Rails loading my associated objects with .includes()?

Time:10-29

My Problem

I'm attempting to retrieve data through a foreign key association in my Ruby on Rails application. The data from the primary table is loaded correctly, but the associated objects are not being loaded and are always nil.

Background Info (Migrations, Database tables, and Model classes)

I'm currently working with two tables:

  • eval_forms
  • user_details

The tables are created through Rails migrations.
The user_details table is created through a single migration:

class CreateUserDetails < ActiveRecord::Migration[5.2]
  def change
    create_table :user_details do |t|
      t.string :eduPersonPrincipalName, unique: true
      t.string :DisplayName, default: 'NULL'
      t.string :Email, default: 'NULL'
      t.string :Role, default: 'Student'
      t.boolean :hasAppointment, default: '0'
      t.timestamps
    end
  end

  def self.down
    drop_table :user_details
  end
end

and the eval_forms table has had a few migrations to create and update it:

class CreateEvalForms < ActiveRecord::Migration[6.1]
  def change
    create_table :eval_forms do |t|
      t.belongs_to :form_builder, foreign_key: 'form_builder_id'
      t.belongs_to :course, foreign_key: 'course_id'
      t.string :Description
      t.datetime :OpenDate
      t.datetime :CloseDate

      t.timestamps
    end
  end
end
class UpdateEvalForms < ActiveRecord::Migration[6.1]
  def change
    add_column :eval_forms, "Author_user_details_id", :bigint, null: false
    add_foreign_key :eval_forms, :user_details, column: "Author_user_details_id"

    add_column :eval_forms, "Year", :integer
    add_column :eval_forms, "Semester", :string
    add_column :eval_forms, "IsArchived", :boolean
  end
end

I know that the foreign key is set up correctly as it is listed correctly in MySQL. Here's a reference from MySQL of the 2 tables and their relation:

Additionally, I've set up the model classes in my Rails app.

eval_form:

class EvalForm < ApplicationRecord
  has_many :eval_forms_roles
  has_many :roles, through: :eval_forms_roles
  has_many :eval_forms_courses
  has_many :courses, through: :eval_forms_courses
  has_many :eval_responses
  has_many :eval_reminders
  belongs_to :user_detail

  validates :formName, presence: true
  validates :formData, presence: true
end

user_detail:

class UserDetail < ApplicationRecord
  has_one :la_detail
  has_many :eval_responses
  has_many :eval_forms
end

So What's Wrong?

Lastly, here is the code to retrieve the objects from the database and the section where I'm getting my error.

My controller action:

 def index
    #  list *all* existing evaluation forms, with options to filter by OpenDate, CloseDate, etc (todo)
    @EvalForms = EvalForm.includes(:user_detail)
  end

My view:

<td><%= ef.user_detail.DisplayName %></td>

My error:

NoMethodError in Evaluations::EvalForms#index undefined method `DisplayName' for nil:NilClass

Extracted Source location: <td><%= ef.user_detail.DisplayName %></td>

Restating the problem

In conclusion, I'm really confused as to why the associated user_detail objects are not being retrieved despite my .includes() statement in the controller action. I'm pretty new to Ruby as well as Rails, but there are other sections in the application that look similar to this and work correctly so I don't see what my issue is.

CodePudding user response:

I would start by using conventional naming which in Rails means snake_case everywhere:

class CreateUserDetails < ActiveRecord::Migration[5.2]
  def change
    create_table :user_details do |t|
      t.string  :edu_person_principal_name, unique: true
      t.string  :display_name 
      t.string  :email
      t.string  :role, default: 'Student'
      t.boolean :has_appointment, default: false # let the driver handle conversion
      t.timestamps
    end
  end
end
class UpdateEvalForms < ActiveRecord::Migration[6.1]
  def change
    change_table :eval_forms do |t|
      t.belongs_to :author_user_details, foreign_key: { to_table: :user_details }
      t.integer    :year # consider using `YEAR(4)` instead
      t.string     :semester
      t.boolean    :is_archived, default: false
    end
  end
end

If you continue using a strange mix of camelCase and PascalCase you will need to explicitly configure all your associations and you will lose all the advantages of convention over configuration. I would not recommend this at all unless you're stuck with a legacy database as its a surefire recipe for developer confusion and bugs.

You will also get a missing constant error if you call the PascalCase methods without an explicit recipient (self):

class EvalForm < ApplicationRecord
  def short_description
    # uninitialized constant Description (NameError)
    Description.truncate(27, separator: ' ')
  end
end

While you can fix this with self.Description.truncate(27, separator: ' ') its still very smelly.

In this case if you want to call the column author_user_details_id instead of user_details_id which is derived from the name you need to configure the assocation to use the non-conventional name:

class EvalForm < ApplicationRecord
  belongs_to :user_detail, foreign_key: :author_user_details_id
end
class UserDetail < ApplicationRecord
  has_many :eval_forms, foreign_key: :author_user_details_id
end

If the rest of your schema looks like this you'll have to do this across the board.

  • Related