Understanding Rails Associations and Their Helper Methods — Ruby Deep Dive [22]
Associations in Ruby on Rails are powerful features that allow you to define relationships between different models in your application. By defining associations, you can streamline your code and make it more intuitive by using a range of automatically generated helper methods. These methods allow for easy creation, access, and management of related objects. In this article, we’ll take a deep dive into Rails associations, exploring the different types of associations, the helper methods they provide, and how these methods simplify querying and modifying related objects.
1. Types of Associations in Rails
Rails provides several types of associations to define relationships between models:
belongs_to
: Indicates that a model has a foreign key that points to another model.has_one
: Indicates a one-to-one connection with another model.has_many
: Indicates a one-to-many connection with another model.has_many :through
: Sets up a many-to-many connection with another model through a third model.has_and_belongs_to_many
: Directly sets up a many-to-many connection with another model without a join model.has_one :through
: A one-to-one relationship through another model.
Each type of association generates a set of helper methods that allow for easy interaction with the related models.
2. Understanding Helper Methods Generated by Associations
When you define associations in your models, Rails automatically generates a variety of methods that help you interact with the associated records. Let’s explore these methods in detail.
belongs_to
Association Helper Methods
When a model uses belongs_to
, it signifies that it holds a reference to another model through a foreign key. For instance, consider the following models:
class Menu < ApplicationRecord
belongs_to :restaurant
end
class Restaurant < ApplicationRecord
has_many :menus
end
With the belongs_to :restaurant
association, Rails provides several helper methods:
restaurant
: Returns the associatedRestaurant
object for theMenu
instance.
@menu = Menu.find(1)
@restaurant = @menu.restaurant
restaurant=
: Assigns aRestaurant
object to themenu
instance.
@menu.restaurant = Restaurant.find(2)
build_restaurant
: Instantiates a newRestaurant
object and assigns it to themenu
without saving either object to the database.
@menu.build_restaurant(name: "New Restaurant")
create_restaurant
: Creates a newRestaurant
object, assigns it to themenu
, and saves both objects to the database.
@menu.create_restaurant(name: "New Restaurant")
create_restaurant!
: Similar tocreate_restaurant
, but raises an exception if validation fails.
has_many
Association Helper Methods
The has_many
association is used when a model can be associated with multiple instances of another model. For example:
class Restaurant < ApplicationRecord
has_many :menus
end
With the has_many :menus
association, Rails provides the following methods:
menus
: Returns a collection ofMenu
objects associated with theRestaurant
.
@restaurant = Restaurant.find(1)
@menus = @restaurant.menus
menus<<
: Adds one or moreMenu
objects to the association collection.
@restaurant.menus << Menu.new(name: "Dinner Menu")
menus.delete
: Removes one or more objects from the collection by removing the association in the database, not deleting the objects themselves.
@restaurant.menus.delete(@menu)
menus.destroy
: Similar todelete
, but also destroys the associated objects.
@restaurant.menus.destroy(@menu)
menus.create
: Creates and saves a newMenu
object associated with theRestaurant
.
@restaurant.menus.create(name: "Lunch Menu")
menus.create!
: Similar tocreate
, but raises an exception if validation fails.menus.build
: Instantiates a newMenu
object associated with theRestaurant
without saving it.
@menu = @restaurant.menus.build(name: "Brunch Menu")
menus.count
,menus.size
,menus.empty?
,menus.exists?
: Provides various ways to query the association for the number of related records, check if there are any records, or if a specific record exists.
@restaurant.menus.count
@restaurant.menus.exists?(name: "Dinner Menu")
has_one
Association Helper Methods
The has_one
association establishes a one-to-one relationship between models. For example:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
With the has_one :account
association, the following methods are available:
account
: Returns the associatedAccount
object.
@supplier.account
account=
: Assigns anAccount
object to thesupplier
.
@supplier.account = Account.find(1)
build_account
: Instantiates a newAccount
object and assigns it to thesupplier
without saving either object.create_account
: Creates and saves a newAccount
object associated with thesupplier
.create_account!
: Similar tocreate_account
, but raises an exception if validation fails.
has_and_belongs_to_many
Association Helper Methods
has_and_belongs_to_many
is used to set up a direct many-to-many relationship without a join model:
class Article < ApplicationRecord
has_and_belongs_to_many :categories
end
class Category < ApplicationRecord
has_and_belongs_to_many :articles
end
With has_and_belongs_to_many :categories
, Rails provides the following methods:
categories
: Returns all associatedCategory
objects.categories<<
: Adds one or more categories to the association.categories.delete
: Removes one or more categories from the association.categories.destroy
: Destroys the association and deletes the categories.categories.create
: Creates a new associatedCategory
.categories.create!
: Creates a new associatedCategory
, raising an exception if validation fails.categories.empty?
,categories.size
,categories.count
: Query methods for checking the status of the association.
has_many :through
and has_one :through
Association Helper Methods
has_many :through
and has_one :through
associations allow for more sophisticated relationships, utilizing a join model:
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
With has_many :through
, you get all the same helper methods as a regular has_many
association (patients
, patients<<
, patients.create
, etc.), but the association goes through the join model (appointments
in this case).
3. How Reverse Associations Work
Understanding how to retrieve associated records in the opposite direction is equally important. When you define an association, Rails provides methods to access and manipulate the reverse relationship.
For example, with the following belongs_to
and has_many
association:
class Menu < ApplicationRecord
belongs_to :restaurant
end
class Restaurant < ApplicationRecord
has_many :menus
end
When you call @menu.restaurant
, Rails uses the foreign key (restaurant_id
) in the menus
table to look up the corresponding Restaurant
record. This lookup is handled by Active Record and is done efficiently using SQL queries.
Similarly, when you call @restaurant.menus
, Rails retrieves all Menu
records with the restaurant_id
matching the Restaurant
’s ID.
4. Polymorphic Associations and Their Methods
Polymorphic associations allow a model to belong to more than one other model using a single association.
For example:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
class User < ApplicationRecord
has_many :pictures, as: :imageable
end
With polymorphic associations, Rails provides:
imageable
: Returns the object thePicture
belongs to, whether it’s aProduct
or aUser
.imageable=
: Assigns an object to thePicture
polymorphic association.
Polymorphic methods behave similarly to other Active Record methods but are flexible enough to handle associations across different models.
5. Delegated Types and Their Methods
Delegated types offer an alternative to Single Table Inheritance (STI) by solving the issue of attribute bloat when a single table holds attributes for multiple subclasses. Instead of creating a single table for different subclasses with potentially unused attributes, delegated types let you split attributes across multiple tables.
In this setup, a base class, such as Entry
, holds common attributes, while each subclass, like Message
or Comment
, has its own table for specific attributes.
Example:
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[Message Comment], dependent: :destroy
end
class Message < ApplicationRecord
include Entryable
def title
subject
end
end
class Comment < ApplicationRecord
include Entryable
def title
content.truncate(20)
end
end
With delegated types, Rails automatically provides the following helper methods:
entryable
: Returns the delegated object (eitherMessage
orComment
).entryable_class
: Returns the class of the delegated object.entryable_name
: Returns the name of the subclass, like"message"
or"comment"
.Entry.messages
: Retrieves all entries whereentryable_type
is"Message"
.Entry#message?
: Returnstrue
if theentryable_type
is"Message"
, otherwisefalse
.Entry#message
: Returns theMessage
record, if applicable.
Delegated types allow you to organize related models without overloading a single table, providing cleaner, more flexible associations while still benefiting from Active Record’s delegation features.
The Difference Between STI and Delegated Types
- STI: Multiple models (subclasses) use one table, which leads to storing a lot of unnecessary columns (For example, if only
Message
has asubject
and onlyComment
hascontent
, both fields would still have to be in theentries
table. This results in attribute bloat). - Delegated Types: Each model gets its own table for specific attributes, but they share a base class with only common attributes, reducing complexity and bloat.
Conclusion
Understanding the helper methods generated by Rails associations is crucial for efficient Rails development. These methods provide powerful ways to interact with related data without writing complex queries or handling SQL directly. By leveraging associations and their methods, you can write more readable, maintainable, and robust Rails applications. Whether you’re setting up simple one-to-many relationships or complex many-to-many relationships with join models, Rails associations offer a powerful toolkit to manage your data relationships effectively.