Search
⌘K
    to navigateEnterto select Escto close

    Defining associations and best practices

    What is an association in Rails?

    In Rails, an association can be defined as a relationship between two ActiveRecord models. For example, in the Granite application, a task created by a user. And each user can create multiple tasks thus building a relationship between the User and Task models.

    We will establish this relationship with the help of an association later in this chapter.

    Types of associations

    Rails support six types of associations:

    belongs_to association

    A belongs_to association sets up a connection with another model, such that each instance of the declaring model belongs to one instance of the other model. For example, in a school database that has a student table and a group table, each record in the student table belongs to a record in the group table.

    A belongs_to association for the above example can be declared as follows:

    1class Student
    2  belongs_to :group
    3end

    Note, that the name of a belongs_to association must always be a singular term. This is because a belongs_to association creates a relationship with a single record. Rails knows this and infers the class name from the association name itself. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.

    The corresponding migration would look like so:

    1class CreateResults < ActiveRecord::Migration
    2  def change
    3    create_table :students do |t|
    4      t.references :group, null: false, foreign_key: true
    5
    6      t.timestamps
    7    end
    8  end
    9end

    In the above migration t.references :group will add a column named group_id to the students table, which will store the primary key of the corresponding group record. By passing option foreign_key: true, a foreign key constraint is added and null: false will ensure that a foreign_key is present.

    You can pass certain options to the belongs_to association. Here we will discuss some important and frequently used options. These are as follows:

    1. class_name: If the name of the other model cannot be derived from the association name, you can use the :class_name option to supply the model name. For example, if a student belongs to a cohort, but the actual name of the model containing students is group, you'd set things up this way:
    1class Student < ActiveRecord::Base
    2  belongs_to :cohort, class_name: "Group"
    3end

    In other words when you use an alias of the model name to name the association, then you should pass the actual model name in the class_name option to make Rails aware of the related model.

    However, you shouldn't do so unless you have a strong reason for it. It is a Rails convention to name the association after the model.

    1. foreign_key: By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix _id added. The :foreign_key option lets you set the name of the foreign key directly if you are using an alias for the association name like so:
    1class Student < ActiveRecord::Base
    2  belongs_to :cohort, class_name: "Group", foreign_key: "group_id"
    3end

    Keep in mind that doing so will not create a foreign key column called student_id inside the result table. It can only be done using a migration.

    1. primary_key: Rails assumes that the id column is used to hold the primary key of its tables. Suppose the primary key of the groups table is stored in the g_id instead of the id column then you can use the :primary_key option to specify g_id whose value should be used as the primary key like so:
    1class Student < ActiveRecord::Base
    2  belongs_to :group, primary_key :g_id
    3end
    1. optional: Consider a scenario where a student isn't a part of a group. In that case, you can declare an optional association. For the optional association to work, there shouldn't be a not null constraint on the foreign_key column in the table for the declaring model. For such cases you can declare the association like so:
    1class Student < ActiveRecord::Base
    2  belongs_to :group, optional: true
    3end
    1. polymorphic: Passing true to the :polymorphic option indicates that this is a polymorphic association. Polymorphic associations are discussed in detail later in this chapter.

    has_one association

    A has_one association sets up a one-to-one relationship with another model, such that each instance of the declaring model contains one instance of the other model. For example, in a school database that has a student table and a result table, each record in the students table contains a record in the results table.

    A has_one association for the above example can be declared as follows:

    1class Student
    2  has_one :result
    3end

    The name of a has_one association must always be a singular term. Because a has_one association creates a relationship with a single record of the other model.

    The corresponding migration would look like:

    1class CreateResults < ActiveRecord::Migration
    2  def change
    3    create_table :students do |t|
    4    end
    5
    6    create_table :results do |t|
    7      t.references :student, null: false, foreign_key: true
    8
    9      t.timestamps
    10    end
    11  end
    12end

    The above migration will create a student_id column in the results table which will store the primary key of the corresponding student record.

    Note that the foreign key column will always be present inside the table for the class declaring the belongs_to association. In this case it is the results table.

    You can also pass certain options to the has_one association. Some important options which you might need to pass often are as follows:

    1. class_name: This option is used to specify the name of the model class when it cannot be inferred from the association name. For example, if a student belongs to a cohort, but the actual name of the model containing students is group, you'd set things up this way:
    1class Student < ActiveRecord::Base
    2  has_one :final_result, class_name: "Result"
    3end

    You should always name the association after the model as it is the Rails convention.

    1. primary_key: By convention, Rails assumes that the column used to hold the primary key of a model is id. You can override this and explicitly specify the primary key with the :primary_key option.

      For example, if the students table has id as the primary_key but it also has a st_id column. The requirement is that the results table should hold the st_id column value as the foreign key and not id value. This can be achieved like this:

    1class Student < ApplicationRecord
    2  has_one :result, primary_key: :st_id
    3end
    1. foreign_key: By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix _id added. For example, if the name of the foreign key column in the results table is st_id instead of student_id then :foreign_key option lets you set the name of the foreign key directly like so:
    1class Student < ActiveRecord::Base
    2  has_one :final_result, foreign_key: :st_id
    3end

    Keep in mind that doing so will not create a foreign key column called student_id inside the result table. It can only be done using a migration.

    1. dependent: This option controls what happens to the associated record when its owner is destroyed:

      • dependent: :destroy causes the associated object to also be destroyed.

      • dependent: :delete causes the associated object to be deleted directly from the database (so callbacks will not execute).

      • dependent: :nullify causes the foreign key to be set to NULL. Callbacks are not executed.

      • dependent: :restrict_with_exception causes an exception to be raised if there is an associated record.

      • dependent: :restrict_with_error causes an error to be added to the owner if there is an associated object.

    2. as: Setting the :as option indicates that this is a polymorphic association. Polymorphic associations are discussed in detail later in this chapter.

    3. through: The :through option specifies a join model through which to perform the query. We will learn more about this option in the has_one :through associations which we have discussed in detail later in this chapter.

    has_many association

    A has_many association establishes a one-to-many relationship with another model. You'll often find this association on the other side of a belongs_to association. This association indicates that each instance of the model has zero or more instances of another model. For example, a one-to-many relationship between students and projects can be declared using a has_many association like so:

    1class Student < ActiveRecord::Base
    2  has_many :projects
    3end

    The association name should be plural because a parent record can have multiple associated records.

    The corresponding migration would look like so:

    1class CreateProjects < ActiveRecord::Migration[6.0]
    2  def change
    3    create_table :projects do |t|
    4      t.references :student, null: false, foreign_key: true
    5
    6      t.timestamps
    7    end
    8  end
    9end

    The above migration will create a foreign key column called student_id in the projects table which will store the primary key of the corresponding student record.

    Note that the foreign key column will always be present inside the table for the class declaring the belongs_to association. We don't need to mention anything in student table migration because Student should be created first before projects are created.

    You can pass certain options to the has_one association. Some important options which you might need to pass often are as follows:

    1. class_name: This option is used to specify the name of the model class when it cannot be inferred from the association name. For example, if a student belongs to a cohort, but the actual name of the model containing students is group, you'd set things up this way:
    1class Student < ActiveRecord::Base
    2  has_many :assignments, class_name: "Project"
    3end

    You should always name the association after the model as it is the Rails convention.

    1. foreign_key: By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix _id added. For example, if the name of the foreign key column in the results table is st_id instead of student_id then :foreign_key option lets you set the name of the foreign key directly like so:
    1class Student < ActiveRecord::Base
    2  has_many :projects, foreign_key: :st_id
    3end

    Keep in mind that doing so will not create a foreign key column called student_id inside the result table. It can only be done using a migration.

    1. primary_key: By convention, Rails assumes that the column used to hold the primary key of a model is id. You can override this and explicitly specify the primary key with the :primary_key option.

      For example, if the students table has id as the primary_key but it also has a st_id column. The requirement is that the results table should hold the st_id column value as the foreign key and not id value. This can be achieved like this:

    1class Student < ApplicationRecord
    2  has_one :result, primary_key: :st_id
    3end
    1. dependent: This option controls what happens to the associated record when its owner is destroyed:

      • dependent: :destroy causes the associated objects to also be destroyed.

      • dependent: :delete_all causes the associated objects to be deleted directly from the database (so callbacks will not execute).

      • dependent: :nullify causes the foreign key to be set to NULL. Callbacks are not executed.

      • dependent: :restrict_with_exception causes an exception to be raised if there is an associated record.

      • dependent: :restrict_with_error causes an error to be added to the owner if there is an associated object.

    2. as: Setting the :as option indicates that this is a polymorphic association. Polymorphic associations are discussed in detail later in this chapter.

    3. through: The :through option specifies a join model through which to perform the query. We will learn more about this option in the has_many :through associations associations which we have discussed in detail later in this chapter.

    The has_and_belongs_to_many association

    A has_and_belongs_to_many association creates a direct many-to-many connection with another model. This association indicates that each instance of the declaring model refers to zero or more instances of another model. For example, consider a hospital where a doctor can have many patients and a patient can have many doctors too. This many-to-many relationship can be declared using a has_and_belongs_to_many association like so:

    1class Doctor < ApplicationRecord
    2  has_and_belongs_to_many :patients
    3end
    4
    5class Patient < ApplicationRecord
    6  has_and_belongs_to_many :doctors
    7end

    The association name should always be plural because there can be multiple related entities.

    A many-to-many relationship is established with the help of a join table. A join table is created where each record contains the primary keys of related patient and doctor records.

    You need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the :join_table option in the has_and_belongs_to_many association, Active Record creates the name by using the lexical order of the pluralized class names. So a join between doctor and patient models will give the default join table name of "doctors_patients" because "d" outranks "p" in lexical ordering.

    The corresponding migration might look like this:

    1class CreateDoctorsAndPatients < ActiveRecord::Migration[6.0]
    2  def change
    3    create_table :doctors do |t|
    4      t.string :name
    5      t.timestamps
    6    end
    7
    8    create_table :patients do |t|
    9      t.string :name
    10      t.timestamps
    11    end
    12
    13    create_table :doctors_patients, id: false do |t|
    14      t.references :doctor, null: false, foreign_key: true
    15      t.references :patient, null: false, foreign_key: true
    16    end
    17
    18    add_index :doctors_patients, :doctor_id
    19    add_index :doctors_patients, :patient_id
    20  end
    21end

    In the above migration, doctors_patients is the join table. We have passed id: false argument to the create_table method so that the new table does not have a id column for storing the primary key.

    We pass id: false to create_table because that table does not represent a model. Rails will use the join table internally to fetch records. For example, when doctor.patients is called, Rails will internally search all the records within the join table where doctor's id is a match and return the corresponding patient records.

    We have also indices on doctor_id and patient_id column because data will be queried from the doctors_patients table using the values in those columns and adding indices will improve database read speeds.

    Note that we shouldn't be creating combined migrations like this in one file. Migrations should be atomic in the sense that a single migration should not affect multiple tables like in the example shown above. Above code is only for demonstration purposes.

    You can also use the create_join_tables method to create a join table like so:

    1class CreateDoctorsPatientsJoinTable < ActiveRecord::Migration[6.0]
    2  def change
    3    create_join_table :doctors, :patients do |t|
    4      t.index :doctor_id
    5      t.index :patient_id
    6    end
    7  end
    8end

    There are a number of options you can pass to the has_and_belongs_to_many association:

    1. association_foreign_key: By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to the other model is the name of that model with the suffix _id added.

    If the foreign key column in the join table that stores the doctor's id is called physician_id rather than doctor_id then, you can use the :association_foreign_key option inside the Patient model to set the name of the foreign key directly like so:

    1class Patient < ApplicationRecord
    2  has_and_belongs_to_many :doctors, association_foreign_key: :physician_id
    3end
    1. foreign_key: By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix _id added.

    If the foreign key column in the join table that stores the doctor's id is called physician_id rather than doctor_id then. you can use the :foreign_key option inside the Doctor model to set the name of the foreign key directly like so:

    1class Doctor < ApplicationRecord
    2  has_and_belongs_to_many :patients, foreign_key: :physician_id
    3end
    1. class_name: If the name of the other model cannot be derived from the association name, you can use the :class_name option to supply the model name.

    For example, if a patient has many physicians, but the actual name of the model containing assemblies is Doctor, then you can use the :class_name option to explicitly specify the actual model name like so:

    1class Patient < ApplicationRecord
    2  has_and_belongs_to_many :physicians, class_name: "Doctor"
    3end
    1. join_table: If the default name of the join table, based on lexical ordering, is not what you want, you can use the :join_table option to override the default.

    For example, in the above example, if we wanted to name the join table patients_doctors then we could have used the join_table option like so:

    1class Doctor < ApplicationRecord
    2  has_and_belongs_to_many :patients, join_table: :patients_doctors
    3end
    4
    5class Patient < ApplicationRecord
    6  has_and_belongs_to_many :doctors, join_table: :patients_doctors
    7end

    The has_many :through association

    A has_many :through association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model. For example, consider a hospital where patients have appointments with doctors. Patients and doctors share a many-to-many relationship with each other as well as with appointments. The relevant association declarations could look like this:

    1class Doctor < ApplicationRecord
    2  has_many :appointments
    3  has_many :patients, through: :appointments
    4end
    5
    6class Appointment < ApplicationRecord
    7  belongs_to :doctor
    8  belongs_to :patient
    9end
    10
    11class Patient < ApplicationRecord
    12  has_many :appointments
    13  has_many :doctor, through: :appointments
    14end

    The corresponding migration will look like so:

    1class CreateAppointments < ActiveRecord::Migration[6.0]
    2  def change
    3    create_table :doctors do |t|
    4      t.string :name
    5      t.timestamps
    6    end
    7
    8    create_table :patients do |t|
    9      t.string :name
    10      t.timestamps
    11    end
    12
    13    create_table :appointments do |t|
    14      t.references :doctor, null: false, foreign_key: true
    15      t.references :patient, null: false, foreign_key: true
    16      t.datetime :appointment_date
    17      t.timestamps
    18    end
    19  end
    20end

    You must be wondering why we didn't use a has_and_belongs_to_many association to declare a many-to-many relationship between the doctors and patients like we did in the last section. If you recall, for has_and_belongs_to_many to work, we have to declare a join table. Although the join table would look very similar to the appointments table, it would be used internally by Rails since we don't declare a corresponding join model for the join table to work with it.

    In this case, we want to work with appointments as a separate entity. That is why we have declared a separate model for appointments. This also allows us to have validations, callbacks and extra attributes inside the appointments model.

    You should use the has_many :through association when you want to work with the join model as a separate entity.

    If you notice has_many :through is actually a has_many association with a through option. You can use all the options of a has_many association along with the through option.

    The has_one :through association

    A has_one :through association sets up a one-to-one connection with another model. This association indicates that the declaring model can be matched with one instance of another model by proceeding through a third model. For example, if each supplier has one account, and each account is associated with one account history, then the supplier model could look like this:

    1class Supplier < ApplicationRecord
    2  has_one :account
    3  has_one :account_history, through: :account
    4end
    5
    6class Account < ApplicationRecord
    7  belongs_to :supplier
    8  has_one :account_history
    9end
    10
    11class AccountHistory < ApplicationRecord
    12  belongs_to :account
    13end

    The corresponding migration might look like so:

    1class CreateAccountHistories < ActiveRecord::Migration[6.0]
    2  def change
    3    create_table :suppliers do |t|
    4      t.string :name
    5      t.timestamps
    6    end
    7
    8    create_table :accounts do |t|
    9      t.references :supplier, null: false, foreign_key: true
    10      t.string :account_number
    11      t.timestamps
    12    end
    13
    14    create_table :account_histories do |t|
    15      t.references :account, null: false, foreign_key: true
    16      t.integer :credit_rating
    17      t.timestamps
    18    end
    19  end
    20end

    Doing so will enable you to fetch the account_history for a supplier using supplier.account_history.

    Polymorphic associations

    So far we have seen how to leverage associations in Rails to build one-to-one, one-to-many and many-to-many associations in Rails. Let's see how we can declare a polymorphic relation in Rails. Polymorphic associations declare a relationship between entities where one entity can belong to multiple entities.

    For example, in a company database, a picture can either belong to an employee or a product. The relevant association can be declared like so:

    1class Picture < ApplicationRecord
    2  belongs_to :imageable, polymorphic: true
    3end
    4
    5class Employee < ApplicationRecord
    6  has_one :pictures, as: :imageable
    7end
    8
    9class Product < ApplicationRecord
    10  has_one :pictures, as: :imageable
    11end

    To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface:.The corresponding migration can be declared like so:

    1class CreatePictures < ActiveRecord::Migration[6.0]
    2  def change
    3    create_table :pictures do |t|
    4      t.string  :name
    5      t.bigint  :imageable_id
    6      t.string  :imageable_type
    7      t.timestamps
    8    end
    9
    10    create_table :employees do |t|
    11      t.string :name
    12    end
    13
    14    create_table :products do |t|
    15      t.string :name
    16    end
    17
    18    add_index :pictures, [:imageable_type, :imageable_id]
    19  end
    20end

    Rather than specifying the imageable_id and imageable_type columns in the above migration, we could have simplified it like so:

    1create_table :pictures do |t|
    2  t.string  :name
    3  t.references :imageable, null: false, foreign_key: true, polymorphic: true
    4  t.timestamps
    5end

    Passing polymorphic: true to t.references is the same as specifying imageable_id and imageable_type columns separately.

    In the above example, you can retrieve a collection of pictures from an instance of the Employee model using @employee.pictures.

    Similarly, you can retrieve using a product object using @product.pictures.

    If you have an instance of the Picture model, you can get to its parent via @picture.imageable.

    The above example denotes a one-to-many polymorphic association. But polymorphic associations aren't restricted to one-to-many relationships and they can exist between one-to-one and many-to-many relationships as well. A polymorphic relation isn't a separate category of relations between models. It is in fact a subset of the other types of relationships.

    Defining associations in User model

    In the Granite application, a user can have many tasks assigned to them and from Rails perspective, those tasks belong to a user.

    Since a user can have many tasks assigned to them, we will declare a has_many association called assigned_tasks in the User model.

    No need to worry about assigned_tasks now. It's a custom association name. We will discuss about why we have used a custom association name after adding the code to the respective file.

    Add the following line into the user.rb file:

    1class User < ApplicationRecord
    2  has_many :assigned_tasks, foreign_key: :assigned_user_id, class_name: "Task"
    3
    4  validates :name, presence: true, length: { maximum: 35 }
    5end

    By default Rails infers the associated class's name from the association name itself and the foreign key to be a column in the associated table with the same name as that of the declaring class in lower-case and _id suffixed.

    But we have used a custom association name hence Rails cannot infer the aforementioned details from the association name. That's why we have passed the class_name and foreign_key options to the has_many association.

    1class User
    2  has_many :tasks
    3end

    Note that, it is suggested to name associations after the class name for simplicity but we have deferred from that here because later in this book we will be adding another association between the User model and the Task model for task owner. Hence, we have named the association accordingly to avoid confusion, given that later there will be two foreign keys between Task and User models.

    Similarly we can declare a belongs_to association in the Task model. Add the following line into the task.rb file:

    1class Task < ApplicationRecord
    2  belongs_to :assigned_user, foreign_key: "assigned_user_id", class_name: "User"
    3
    4  # previous code
    5
    6  private
    7
    8    # previous code
    9end

    Don't get confused by the plural and singular association names. In Rails we follow certain naming conventions. To understand the naming convention we have used for associations you can refer to naming model associations section in the in-depth chapter where we have summarized most of the Rails naming conventions. You can refer to it whenever you are confused about the naming and come back.

    Create child record using parent association

    Let us understand how we can leverage associations to create child records using parent records. In the Granite application, a user has many assigned_tasks. You can create a new assigned_task for a certain user like so:

    1Task.create(task_attributes.merge(assigned_user_id: user.id))

    Or you can use the assigned_tasks association declared in the User model to create a new task for the user like so:

    1user.assigned_tasks.create(task_params)

    In the latter approach you don't need to explicitly pass the user id while creating a new task record. It looks more concise and clean.

    This is possible because association methods such as belongs_to, has_many etc are macros. Macros in Ruby are class methods that generate instance methods. To learn about how macros works in depth you can refer to the Rails macros and metaprogramming chapter in this book.

    collection.create is one of the instance methods that is added to the declaring model when you declare a has_many association. collection is replaced by the association name.

    There are several other methods as well which you can use in your code for performing CRUD operations on data. For example, you can use the getter and setter methods added by the has_many association in the User model like so:

    1user.assigned_tasks  // returns a collection of all assigned tasks of the user
    2user.assigned_tasks=collection_of_task_records  // saves a list of tasks as assigned tasks of the user

    It is considered good practice to use these instance methods added by associations to perform various operations on data.

    For a list of methods added by each association in the declaring model you can refer to the official documentation.

    Now let's commit the changes:

    1git add -A
    2git commit -m "Added association in User model for assigned tasks"
    Previous
    Next