Tips Tricks and Warnings in Associations

 

3 Tips, Tricks, and Warnings

Here are a few things you should know to make efficient use of Active Record associations in your Rails applications:

  • Controlling caching
  • Avoiding name collisions
  • Updating the schema
  • Controlling association scope
  • Bi-directional associations

3.1 Controlling Caching

All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example:

author.books                 # retrieves books from the database
author.books.size            # uses the cached copy of books
author.books.empty?          # uses the cached copy of books

But what if you want to reload the cache, because data might have been changed by some other part of the application? Just call reload on the association:

author.books                 # retrieves books from the database
author.books.size            # uses the cached copy of books
author.books.reload.empty?   # discards the cached copy of books
                             # and goes back to the database

3.2 Avoiding Name Collisions

You are not free to use just any name for your associations. Because creating an association adds a method with that name to the model, it is a bad idea to give an association a name that is already used for an instance method of ActiveRecord::Base. The association method would override the base method and break things. For instance, attributes or connection are bad names for associations.

3.3 Updating the Schema

Associations are extremely useful, but they are not magic. You are responsible for maintaining your database schema to match your associations. In practice, this means two things, depending on what sort of associations you are creating. For belongs_to associations you need to create foreign keys, and for has_and_belongs_to_many associations you need to create the appropriate join table.

3.3.1 Creating Foreign Keys for belongs_to Associations

When you declare a belongs_to association, you need to create foreign keys as appropriate. For example, consider this model:

class Book < ApplicationRecord
  belongs_to :author
end

This declaration needs to be backed up by a corresponding foreign key column in the books table. For a brand new table, the migration might look something like this:

class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.datetime   :published_at
      t.string     :book_number
      t.references :author
    end
  end
end

Whereas for an existing table, it might look like this:

class AddAuthorToBooks < ActiveRecord::Migration[5.0]
  def change
    add_reference :books, :author
  end
end

If you wish to enforce referential integrity at the database level, add the foreign_key: true option to the ‘reference’ column declarations above.

3.3.2 Creating Join Tables for has_and_belongs_to_many Associations

If you create a has_and_belongs_to_many association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the :join_table option, Active Record creates the name by using the lexical order of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering.

The precedence between model names is calculated using the <=> operator for String. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '_' is lexicographically less than 's' in common encodings).

Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations:

class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end
 
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end

These need to be backed up by a migration to create the assemblies_parts table. This table should be created without a primary key:

class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.2]
  def change
    create_table :assemblies_parts, id: false do |t|
      t.bigint :assembly_id
      t.bigint :part_id
    end
 
    add_index :assemblies_parts, :assembly_id
    add_index :assemblies_parts, :part_id
  end
end

We pass id: false to create_table because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a has_and_belongs_to_many association like mangled model IDs, or exceptions about conflicting IDs, chances are you forgot that bit.

You can also use the method create_join_table

class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
  def change
    create_join_table :assemblies, :parts do |t|
      t.index :assembly_id
      t.index :part_id
    end
  end
end

3.4 Controlling Association Scope

By default, associations look for objects only within the current module's scope. This can be important when you declare Active Record models within a module. For example:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end
 
    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end

This will work fine, because both the Supplier and the Account class are defined within the same scope. But the following will not work, because Supplier and Account are defined in different scopes:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end
  end
 
  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end

To associate a model with a model in a different namespace, you must specify the complete class name in your association declaration:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account,
        class_name: "MyApplication::Billing::Account"
    end
  end
 
  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier,
        class_name: "MyApplication::Business::Supplier"
    end
  end
end

3.5 Bi-directional Associations

It's normal for associations to work in two directions, requiring declaration on two different models:

class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :author
end

Active Record will attempt to automatically identify that these two models share a bi-directional association based on the association name. In this way, Active Record will only load one copy of the Author object, making your application more efficient and preventing inconsistent data:

a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
a.first_name = 'David'
a.first_name == b.author.first_name # => true

Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain a scope or any of the following options:

  • :through
  • :foreign_key

For example, consider the following model declarations:

class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end

Active Record will no longer automatically recognize the bi-directional association:

a = Author.first
b = a.books.first
a.first_name == b.writer.first_name # => true
a.first_name = 'David'
a.first_name == b.writer.first_name # => false

Active Record provides the :inverse_of option so you can explicitly declare bi-directional associations:

class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end

By including the :inverse_of option in the has_many association declaration, Active Record will now recognize the bi-directional association:

a = Author.first
b = a.books.first
a.first_name == b.writer.first_name # => true
a.first_name = 'David'
a.first_name == b.writer.first_name # => true

Comments

Popular posts from this blog

Rails 7 Features :: Comparison with Rails 6 and Rails 5