Feature Proposal: `alias_association`

TLDR

There is a high chance of Rails breaking alias_attribute behaviour when its used to alias an association instead of an attribute. This is an opportunity to share reasons to why Rails may need to provide an alternative such as alias_association

Context

In Rails, we currently have the alias_attribute method, which provides a way to alias an attribute. This is a powerful feature that allows developers to refer to an attribute by a different name, providing flexibility in how applications design models and interact with data.

However, there’s currently no built-in way to alias an association. This has led some applications to use alias_attribute as a workaround to alias associations, which is not its intended use. This approach is not officially supported by Rails and could potentially break in future Rails updates with no deprecation cycle.

This lack of a built-in method to alias an association can lead to unnecessary code duplication and can make our models more difficult to understand and maintain.

Blockers

  • :warning: This proposal currently lacks a strong use-case example. Without a solid case it is going to be hard to justify adding complexity that comes with the feature.
  • Even with a strong need the implementation will need to ensure that it doesn’t hurt performance for applications that won’t use the feature

Proposal

I propose that we add a new method to Rails: alias_association. This method would work similarly to alias_attribute, but it would be used to alias an association instead of an attribute.

Here’s an example of how it might be used:

class Post < ApplicationRecord
  belongs_to :author
  alias_association :creator, :author
end

In this example, :creator is an alias for the :author association. This means that we can use post.creator to refer to the post’s author, just like we would use post.author. Along with aliased accessor we will be able to query by the aliased association: Post.where(creator: Author.first).to_a

Acceptance Criteria

  • The alias_association method should create an alias for an association.

  • The alias should work exactly like the original association. This means that we should be able to call all the same methods on the alias as we can on the original association.

  • The alias_association method should raise an error if the original association does not exist.

  • The alias_association method should not interfere with the original association. This means that we should still be able to use the original association even after creating an alias for it.

    Fundamentals

    The alias_association feature should at least provide the same capabilities as alias_attribute when used against an association:

    • Provides getter and setter for the association
    • Allows objects to be queried by the aliased association name in .where()

    Nice to have:

    • We need to make sure that the original foreign key is being set when association is assigned using an aliased setter
    • Overall alias_association should also define an alias on the original foreign key attribute

Acceptance Criteria tests

This is a script to be used to verify the implementation of the feature against acceptance criteria. The list will be extended with more examples and eventually may be used as a set of tests in the Rails test suite.

# typed: true
# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", github: "rails/rails", branch: "main"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :posts, force: true do |t|
    t.string(:title)
    t.text(:body)
    t.integer(:author_id)
  end

  create_table :authors, force: true do |t|
    t.string(:name)
  end
end

class Post < ActiveRecord::Base
  belongs_to :author
  # Uncomment to see some tests pass to show which use-cases are supported by `alias_attribute`
  alias_attribute :creator, :author
  # alias_association :creator, :author
end

class Author < ActiveRecord::Base
  has_many :posts

  # Uncomment to see some tests pass to show which use-cases are supported by `alias_attribute`
  alias_attribute :publications, :posts
  # alias_association :publication, :posts
end

bob = Author.create(name: "Bob")
alice = Author.create(name: "Alice")

bob.posts.create(title: "Bob's first post", body: "Bob says hello")
bob.posts.create(title: "Bob's second post", body: "Bob says hey")
alice.posts.create(title: "Alice's first post", body: "Alice says hello")
alice.posts.create(title: "Alice's second post", body: "Alice says hey")

class AliasAssociation < Minitest::Test
  def setup
    @bob = Author.find_by(name: "Bob")
    @bobs_first_post = Post.find_by(title: "Bob's first post")
    @bobs_second_post = Post.find_by(title: "Bob's second post")
    @alice = Author.find_by(name: "Alice")
    @alices_first_post = Post.find_by(title: "Alice's first post")
  end

  def test_belongs_to_getter
    assert_equal(@bob, @bobs_first_post.author)
    assert_equal(@bobs_first_post.author, @bobs_first_post.creator)
  end

  def test_belongs_to_setter
    @bobs_first_post.creator = @alice

    assert_equal(@alice, @bobs_first_post.author)
    assert_equal(@alice, @bobs_first_post.creator)
  end

  def test_belongs_to_where
    bobs_posts = @bob.posts.to_a
    refute_empty(bobs_posts)
    assert_equal(bobs_posts, Post.where(creator: @bob).to_a)
  end

  def test_has_many_getter
    assert_equal([@bobs_first_post, @bobs_second_post], @bob.posts.to_a)
    assert_equal(@bob.posts.to_a, @bob.publications.to_a)
  end

  def has_many_setter
    post = Post.create(title: "Completely new post")
    @bob.publications = [post]
    @bob.save

    bobs_posts = @bob.posts.to_a
    assert_equal([post], @bob.posts)
    assert_equal([post], @bob.publications)
  end

  def has_many_shovel
    post = Post.create(title: "Additional post")
    @bob.publications << post

    assert_includes(@bob.posts.to_a, post)
    assert_includes(@bob.publications.to_a, post)
  end

  def has_many_where
    bobs_posts = @bob.posts.to_a
    refute_empty(bobs_posts)
    assert_equal(bobs_posts, Post.where(creator: @bob).to_a)
  end

  # TODO:
  # Add tests for aliased `creator_id` foreign key
end

Request for Feedback

To ensure that alias_association meets the needs of the Rails community, we’re interested in hearing about your potential use-cases for this feature. If you’ve ever found yourself wishing for a way to alias an association, or if you’ve used alias_attribute or other workarounds to achieve this, we want to hear from you.

Please share:

  • Scenarios where you would use alias_association
  • How you’re currently handling these scenarios without alias_association
  • Any other thoughts or ideas you have about this feature

Your input will help us shape alias_association into a feature that’s useful and intuitive for all Rails developers. Thank you in advance for your feedback!

2 Likes