Module: ConcernsOnRails::Models::Aliasable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/models/aliasable.rb

Overview

Alias an existing ActiveRecord association under a second name with FULL support — read, write/assign, build/create, and the query side (joins / includes / preload / eager_load / where hash conditions) —not just a delegated reader. Rails’ alias_attribute covers columns only; there is no built-in way to alias an association.

class Book < ApplicationRecord
  include ConcernsOnRails::Aliasable

  belongs_to :author
  has_many :chapters

  alias_association :writer,   :author     # alias_method order: new, old
  alias_association :sections, :chapters

  # Options:
  alias_association :penman, :author, only: :reader            # no writer/build_/create_
  alias_association :parts,  :chapters, except: :ids           # skip part_ids/part_ids=
  alias_association :owner,  :author, deprecated: "use #author"  # warns on use
  alias_association :maker,  :author, alias_foreign_key: true    # maker_id -> author_id
end

book.writer                  # same cached object as book.author
book.writer = user           # assigns through the original association
book.build_writer(...)       # build_/create_/create_!/reload_ (singular)
book.sections << chapter     # the same CollectionProxy as book.chapters
book.section_ids             # ids reader/writer (collection)
Book.joins(:writer)          # INNER JOIN "authors"
Book.joins(:sections).where(sections: { title: "Intro" })

Notes:

* Declare alias_association AFTER the source association — it raises
  "does not exist" when the source has not been defined yet.
* One loaded cache under two names: record.association(:alias) IS
  record.association(:source), and only the source macro installs
  callbacks — dependent:, counter_cache, autosave and validations
  run exactly once.
* Query SQL: a bare joins(:sections) joins "chapters" directly; when
  paired with where(sections: {...}) Rails aliases the join as
  "sections" (INNER JOIN "chapters" "sections"). A where-hash key
  must match the name you joined under (same rule as stock Rails):
  joins(:sections).where(sections: {...}) works,
  joins(:chapters).where(sections: {...}) does not.
* The belongs_to foreign-key attribute is NOT aliased — pair with
  Rails' alias_attribute (e.g. :writer_id, :author_id) if needed.
* has_and_belongs_to_many cannot be aliased — use has_many :through.
* has_many/has_one :through CAN be aliased (the copy pins `source:`
  so it is not re-derived from the alias name). One caveat: when the
  alias is declared before the through model's class has loaded AND
  the through model defines the source under a different name (e.g.
  belongs_to :author behind has_many :authors), declare `source:`
  explicitly on the original association.
* Subclasses inherit aliases. If a subclass redefines the source
  association, re-declare the alias there (re-declaring with the SAME
  source is allowed and idempotent) so the query side picks up the
  new reflection. Repointing an existing alias at a DIFFERENT source
  raises.
* only:/except: narrow the generated methods by group — :reader,
  :writer, :build, :reload (singular), :ids (collection). Groups that
  do not apply to the reflection type are ignored; the query side
  (joins/includes/where-hash) is always registered.
* deprecated: true (or a String hint) makes every generated delegator
  warn through ConcernsOnRails.deprecator before delegating — the
  gradual-rename story: point the OLD name at the new association and
  deprecate it. The query side and alias_foreign_key attribute
  aliases do not warn.
* alias_foreign_key: true (belongs_to only) also aliases the FK
  attribute via Rails' alias_attribute (<alias>_id, plus <alias>_type
  when polymorphic).

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

LABEL =
"ConcernsOnRails::Models::Aliasable".freeze
SINGULAR_STEM_GROUPS =

Method stems per reflection type, keyed by the only:/except: group names; %s is the association name. The reload_/reset_ stems vary across the supported Rails range and build_/create_ are skipped by Rails for polymorphic belongs_to, so each SOURCE method is existence-checked before its delegator is made. The :ids pair is handled separately (it singularizes the association name).

{
  reader: ["%s"].freeze,
  writer: ["%s="].freeze,
  build: ["build_%s", "create_%s", "create_%s!"].freeze,
  reload: ["reload_%s", "reset_%s"].freeze
}.freeze
COLLECTION_STEM_GROUPS =
{
  reader: ["%s"].freeze,
  writer: ["%s="].freeze
}.freeze
METHOD_GROUPS =
%i[reader writer build reload ids].freeze

Instance Method Summary collapse

Instance Method Details

#association(name) ⇒ Object

Route the alias to the source association proxy so record.association(:alias) IS record.association(:source) — one loaded cache. Load-bearing for the preloader, which assigns loaded records via record.association(reflection.name) using the alias’s renamed reflection.



400
401
402
# File 'lib/concerns_on_rails/models/aliasable.rb', line 400

def association(name)
  super(self.class.aliasable_aliases[name.to_sym] || name)
end