Following topics to be covered in this article

Conventional Foreign-key

When we want to create a foreign key in a table, we explicitly set it to true in a migration file where rails automatically creates a foreign key attribute named in the form model_class_name’_id , where ‘model_class_name’ refers to the class name of the model it's associating to.

In the model file, to set an association with another table, we pass model_class_name value to belongs_to method and rails will look for a foreign key attribute, model_class_name’_id in a table where belongs_to is declared.

In my Active Record article, I discussed that the foreign key name is a part of the convention which rails set for us automatically. And when we set an association, rails assumes that we’re following that convention and it links our models accordingly.

Conventional foreign key also helps out our models to have a bi-directional association. (Explained later)

Unconventional Foreign key

However, there are times where we need to override the conventional foreign key name for some reason, meaning having a different foreign key name. And when we do so and we set associations, we need to let rails know which attribute is a foreign key and in which table or model class, then and only then are associations will work as they’re expected to.

For example, let's say we have 2 tables in the database, users & articles, and 2 corresponding model classes User & Article and we set has_many-belongs_to association.

Here we set a foreign key with a custom name. So first, let’s create users table with the following attributes:

rails g model User name:string age:integer gender:string

then articles table with foreign-key name as author_id:

rails g model Article author:references title:string description:text

we give references to author so rails knows to create a foreign key as author_id. And our migration file:

This tell rails that we have author_id as foreign-key but it will look for in authors table in database which doesn’t exist so we need to explicitly let rails know that it should look for author_id in users table. So we modify this line in the migration file:

t.references :author, foreign_key: {to_table: :users}

Then we run the migration and now we set up its associations:

# app/models/user.rb
class User < ApplicationRecord
has_many :articles, foreign_key: 'author_id'
end
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user, foreign_key: 'author_id'
end

has_many and belongs_to takes a model class name that they need to associate with which is also called an association name. We declare :foreign_key in both the models because we need to let rails know that we have set an unconventional foreign key name.

Without :foreign_key option in the model file, when we set a new instance in Articles table of a particular user, we get the following error in the rails console:

# loading author from database
$ user = User.first
# creating new article of this author
$ user.articles.new
=> ActiveModel::UnknownAttributeError: unknown attribute 'user_id' for Article.

The error tells us that Active Record is trying to find user_id in article table but it couldn't find.

Setting up :foreign_key will correctly link to the article model and hence will be able to create new instances:

$ user.articles.new
=> #<Article id: nil, author_id: 1, title: nil, description: nil, created_at: nil, updated_at: nil>

Bi-directional association

Active Record supports automatic identification for most associations with standard association names. However, when we use scope or options like :foreign-key or :through, our models are no longer bi-directionally associated.

So setting up a custom foreign key will set our models to a one-directional association where Active Record will load 2 copies of User and Article class objects instead of 1.

# Creates 1st object of User class
$ user = User.first
=> #<User id: 1, name: "Juzer", created_at: "##", updated_at: "##">
# Creates 2nd object of Article class
$ article = user.articles.first
=> #<Article id: 1, author_id: 1, title: "Foreign-Key", description: nil, created_at: "##", updated_at: "##">
$ user.name == article.user.name
=> true
$ user.object_id == article.user.object_id
=> false
# Setting new name of User object
$ user.name = "Shakir"
=> "Shakir"
# but doesn't update for 2nd object
$ user.name == article.user.name
=> false

Here it only updates User objects’ name but not of Article objects’ because both are different class objects loaded in memory, hence it returns false.

To set up a bi-directional relationship we give :inverse_of option where Active Record will attempt to automatically identify that these two models share a bi-directional association which will then load only one copy of the class object, hence, preventing inconsistent data.

We declare :inverse_of option to the has_many association which is in User model:

class User < ApplicationRecord
has_many :articles, foreign_key: 'author_id', inverse_of: 'user'
end

Now,

$ user.name == article.user.name
=> true
$ user.object_id == article.user.object_id
=> true
$ user.name = "Shakir"
=> "Shakir"
$ user.name == article.user.name
=> true

Association names

The ‘name’ that we pass to an association method is called an association name.

For example:

# 1st model
class Writer < ApplicationRecord
has_many :articles
end
#2nd model
class Article < ApplicationRecord
belongs_to :writer
end

Here the association name is articles for model class Writer. Most of the time when we give an association name, we name it similar to the model class name we’re relating to. So in the above example, we have an Article model to which the declaring model, Writer, is relating to.

Based on the association name, Active Record will generate methods for us to manipulate with data. So from the above example, we extract a writers’ articles by:

# list of articles of a writer
$ writer.articles
# returns boolean
$ writer.articles.empty?
# extracts that particular article
$ writer.articles.find(article_id)

:class_name

However, there will be times where we want to have different model class methods. Let's see an example of that situation:

class AmazonAccount < ApplicationRecord
has_one :amazon_payment
end
class AmazonPayment < ApplicationRecord
belongs_to :amazon_account
end

Now if we want to extract an amazonians’ (an amazon user) payment detail we would have to give a method like this:

amazonian = AmazonAccount.first
amazonian.amazon_payment

It would be better if we could access payment like:

amazonian.payment

So how would we achieve this? It's simple, we first change our association name to payment and let rails know that it needs to point to AmazonPayments model instead of Payments model by :class_name option.

class AmazonAccount < ApplicationRecord
has_one :payment, class: 'AmazonPayments'
end
class AmazonPayment < ApplicationRecord
belongs_to :amazon_account
end

Now, amazonian.payment will work pointing to an appropriate class.

If we wanted to extract an amazonians’ account details through payment, the process would be like this, first:

# extract first amazonians' payment
payment = AmazonPayment.find_by(amazon_account_id: 1)

then to extract amazonians’ data through payment:

payment.amazon_account

Here I think instead of amazon_account method, the method amazonian would be appropriate, so let's set it in AmazonPayment model:

class AmazonPayment < ApplicationRecord
belongs_to :amazonian, class_name: 'AmazonAccount', foreign_key: 'amazon_account_id'
end

Rails will look for amazonian_id because we have given :amazonian value to belongs_to method. So we need to tell rails that we have a foreign key named as amazon_account_id in amazon_payments table.

Custom association names for more than 2 models

Let's take the same example we discussed before but now we add a 3rd model called Person. So the relationship would be like this: A person has an amazon account for which it has one amazon payment account. Here we would like to tell rails to create model methods with the custom association names we will set.

Association would be something like this:

# app/model/person.rb
class Person < ApplicationRecord
has_one :account, class_name: 'AmazonAccount'
has_one :payment, through: :account
end
# app/model/amazon_account.rb
class AmazonAccount < ApplicationRecord
belongs_to :person
has_one :payment, class_name: 'AmazonPayment'
end
# app/model/amazon_payment.rb
class AmazonPayment < ApplicationRecord
belongs_to :amazonian, class_name: 'AmazonAccount', foreign_key: 'amazon_account_id'
end

Active Record will create the following instance methods for model class objects:

person.account, person.payment, amazon_account.person, amazon_account.payment, amazon_payment.amazonian

In Person model, we set up an association with AmazonAccount model through :class_name option and then set up an association with the AmazonPayment model, where for both associations we have set custom association names. In the :through option we give association name as :account and not :amazon_account because as :account is declared before, it gets loaded with a namespace of :account and we don’t need to mention the :class_name option here because in the AmazonAccount model we have already mentioned.

Accessing payment of a person:

$ person = Person.find(1)
$ person.payment

Accessing person through payment:

$ pay = AmazonPayment.find(1)
$ pay.amazonian

Warning for Association Names

Every model class is inherited from class ApplicationRecord which itself is inherited from Base class from ActiveRecord model.

Creating association names that have the same name as instance methods of the Base class would override the method inherited through it which will break things. For instance, attributes and connection would be bad choices for association names, because those names already exist in the list of ActiveRecord::Base instance methods.

Auto-generated methods & options of association

As we discussed earlier that instance methods are generated based on what association name we give.

Singular Associations (one-to-one relationship)

Includes belongs_to & has_one associations which generate the following methods:

where ‘other’ is an association name given as a method to the declaring model

For a detailed explanation of methods & the options provided by belongs_to association, refer to this & for has_one association, refer to this.

Collection Associations (one-to-many/many-to-many relationship)

Includes has_many & has_many-:through & has_and_belongs_to_many (HABTM) associations which generate the following methods:

For a detailed explanation of methods & the options provided by has_many association, refer to this & for has_and_belongs_to_many association, refer to this.

These are basic things we need to know about the AR Associations. It was 3 long articles that covered different aspects of AR Associations, and if you have read all of them, I am very much humbled for it, and thank you sincerely.

If you have any questions or other comments, feel free to leave them below. Also, if you found this article useful, you can share it so others can find it as well.

I have also written articles on more advanced associations Polymorphic, Self Join, and Single Table Inheritance (STI).

--

--

Juzer Shakir
Juzer Shakir

No responses yet