AssociationAccessors
association_accessors is a tool for generating accessors for ActiveRecord model associations based on columns other than the id (primary key).
Dependencies
-
ruby
'>= 2.3.0' -
activerecord
'>= 4.1.7', '< 6.0' -
rspec
'>= 2.0'(for the test matcher)
Table of Contents
- AssociationAccessors
- Dependencies
- Table of Contents
- The Challenge
- Installation
- Config
- Usage
- With belongs_to
- With has_one
- With has_many
- With has_and_belongs_to_many
- With custom association names
- With polymorphic associations
- Test matcher
- Contributing
- Development
- License
The Challenge
You have the following tables:
ActiveRecord::Schema.define do
create_table :companies, force: true do |t|
t.string :serial
t.string :name
# ...
end
create_table :users, force: true do |t|
t.string :serial
t.integer :company_id
t.string :name
# ...
end
endFor security reasons it is decided that the frontend must not see the ids, only the serials. So, when you want to change the company of a user, params will contain something like this:
# <ActionController::Parameters { "serial": "9jco5RMp4K", "user": { company_serial: "MbyDB18lCi" } } permitted: false>Now, if the User model has #company_serial= method, you can simply permit :company_serial.
Using association_accessors it amounts to this:
class User < ActiveRecord::Base
include AssociationAccessors
belongs_to :company
association_accessor_for :company, with_attribute: :serial
endInstallation
Add this line to your application's Gemfile:
gem 'association_accessors'And then execute:
$ bundle
Config
It is possible to set a default value for the keyword with_attribute:, for example in an initializer:
# config/initializers/association_accessors.rb
AssociationAccessors.default_attribute = :serialUsage
- include the module
AssociationAccessors. - define the associations
- call
.association_accessor_formethod with the association name and the identifier attribute to generate the accessor methods:- for singular associations it will generate a reader and a writer along the rule
[association_name]_[attribute_name] - for collection associations it will generate the methods along the rule
[singular_association_name]_[plural_attribute_name]
- for singular associations it will generate a reader and a writer along the rule
These accessors work more or less the same way as the id accessors generated by default for the associations.
There are a few exceptions and some gotchas, though.
With belongs_to
# AssociationAccessors is included in the ApplicationRecord
class User < ApplicationRecord
belongs_to :company, optional: true # only to demonstrate that it works with the association `nil`
association_accessor_for :company, with_attribute: :serial
endYou have the methods #company_id and #company_id= because of the foreign key column. association_accessors generates the methods #company_serial and #company_serial=.
These work in a similar way with the following differences:
-
#company_idis a column,#company_serialis a computed value (theserialof thecompany) - where#company_idcan return value other thannileven without really having acompany,#company_serialcan only return theserialof thecompany(if any) -
#company_id=does not break if there is no company with the given id, only#companywill returnnil- whereas calling#company_serial=with a not existing serial will raiseActiveRecord::RecordNotFounduser.company # => #<Company id: 10, serial: "HVuPpK"> user.company_id # => 10 user.company_serial # => "HVuPpK" user.company_id = 100 # there is no company with id=100 user.company # => nil user.company_id # => 100 user.company_serial # => nil user.company_serial = 'EeNRM' # ActiveRecord::RecordNotFound (Couldn't find Company)
With has_one
class User < ApplicationRecord
has_one :address
association_accessor_for :address, with_attribute: :serial
endThere is no #address_id or #address_id= method for a has_one association. The generated methods (#address_serial, #address_serial=) behave the following way:
-
#address_serialreturns theserialof theaddressif any -
#address_serial=- changes the
user_idcolumn of theadress(es) setting it tonullor to theidof the user as required - raises
ActiveRecord::RecordNotFoundif the givenserialdoes not exist - raises
ActiveRecord::HasOneThroughCantAssociateThroughHasOneOrManyReflectionif association is defined withthrough:option
- changes the
With has_many
class Company < ApplicationRecord
has_many :users
association_accessor_for :users, with_attribute: :serial
endThe generated methods (#user_serials and #user_serials=) will behave exactly the same way the collection_singular_ids accessor generated by rails:
-
#user_idswill return theids of theusers-#user_serialswill return theserials of theusers -
#user_ids=and#user_serials=will both- update the
company_idcolumn of the users: setting it tonullor to theidof the company as required - raise
ActiveRecord::RecordNotFoundif any of the givenids orserials is non-existing - raise
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflectionif association is defined with:throughoption
- update the
With has_and_belongs_to_many
class User < ApplicationRecord
has_and_belongs_to_many :tasks
association_accessor_for :tasks, with_attribute: :serial
endThe generated methods (#task_serials and #task_serials=) will behave exactly the same way the collection_singular_ids accessor generated by rails:
-
#task_idswill return theids of thetasks-#task_serialswill return theserials of thetasks -
#task_ids=and#task_serials=will both- update the join table adding or deleting records as required
- raise
ActiveRecord::RecordNotFoundif any of the givenids orserials is non-existing
With custom association names
class Node < ApplicationRecord
belongs_to :parent, class_name: 'Node'
association_accessor_for :parent, with_attribute: :serial
has_many :children, class_name: 'Node', foreign_key: :parent_id
association_accessor_for :children, with_attribute: :serial
endThe generated methods (#parent_serial, #parent_serial=, #child_serials and #child_serials=) work the same way, as they rely on the association, not the column.
With polymorphic associations
class User < ApplicationRecord
has_one :image, as: :imageable
association_accessor_for :image, with_attribute: :serial
end
class Task < ApplicationRecord
has_many :images, as: :imageable
association_accessor_for :images, with_attribute: :serial
end
class Image < ApplicationRecord
belongs_to :imageable, polymorphic: true
end- The
has_oneandhas_manyparts work just fine, setting theimageable_typeandimageable_idtonilor the class name and theidof the required record - It is not recommended to use
association_accessorsfor thebelongs_topart of polymorphic associations, as the associated class is derived from the actual value of[association_name]_typecolumn, causing unpredictable behavior and raisingNoMethodErrorwhen it isnil
Test matcher
association_accessors ships a test matcher too: #have_association_accessor_for. It surely works with rspec.
RSpec.configure do |config|
config.include AssociationAccessors::Test, type: :model
end
RSpec.describe User, type: :model do
it { should have_association_accessor_for(:company).with_attribute(:serial) }
endSo far, it checks if the reader and writer methods are defined on the subject, nothing more.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/SzNagyMisu/association_accessors.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To test against other activerecord or rspec versions, check the ./Appraisals file and choose one:
$ bundle exec appraisal activerecord-5.0 bin/console
$ bundle exec appraisal activerecord-5.0 rspec
To run a full test suite (including multiple ruby, activerecord and rspec versions), run:
$ rake full_test
$ RUBY_VERSIONS=2.5.3,2.6.1 rake full_test
Setting the environment variable RUBY_VERSIONS overrides the default (2.3.7, 2.4.1, 2.5.1) ruby versions to test against. Note that while the ruby versions must be installed for the test to run, the gems are handled by appraisal.
License
The gem is available as open source under the terms of the MIT License.