Rails Association — Part 3
A deeper look into foreign keys & custom association names and the methods it generates.
Prerequisite:
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
endclass 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'
endclass 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:
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).