Attribute Assignment in Rails

Understanding the Attribute Assignment API in Rails

In this post, we will explore the `AttributeAssignment` module in Rails, which allows you to set an object's attributes by passing in a hash, a feature commonly used by Active Record models. We'll also learn a little metaprogramming along the way.

3 min read

Here's a regular Ruby class with two properties and a constructor. To create a new instance of the class, you have to pass the arguments expected by the constructor, in the same order.

class Person
  attr_accessor :name, :age
  
  def initialize(name, age)
    @name = name
    @age = age
  end
end

# create a new object
akshay = Person.new('Akshay', 31)

However, if you've worked with ActiveRecord models in Rails, you must have noticed that they don't include the initialize method. Still, you can create them using the new method, passing in a hash of attributes in any order.

class Post < ApplicationRecord
end

# create a new post
post = Post.new(title: 'hello world', body: 'how are you?')

If you try this with a plain Ruby class, it throws an error.

akshay = Person.new(name: 'Akshay', age: 31)

# wrong number of arguments (given 1, expected 2) (ArgumentError)

So the question is: How does Rails make it work? 🤔

The AttributeAssignment Module

The answer lies in the ActiveModel::AttributeAssignment module, which includes the assign_atributes method. It allows you to set all the attributes by passing in a hash of attributes. The hash keys must match the attribute names.

class Language
  include ActiveModel::AttributeAssignment
  attr_accessor :title, :author
end

ruby = Language.new
ruby.assign_attributes(title: "Ruby", author: "Matz")

ruby.title # => 'Ruby'
ruby.author # => 'Matz'

ruby.assign_attributes(author: "Yukihiro Matsumoto")

ruby.title # => 'Ruby'
ruby.author # => 'Yukihiro Matsumoto'

Under the hood, the assign_attributes method calls the assign_attribute method for each key-value pair.

Here's the internal implementation of this method.

def _assign_attribute(k, v)
  setter = :"#{k}="
  if respond_to?(setter)
    public_send(setter, v)
  else
    raise UnknownAttributeError.new(self, k.to_s)
  end
end

Let's try to understand what's going on when we pass name: 'Akshay' when initializing the object.

  1. The setter will be set to :name=
  2. The respond_to? method checks if our class includes the name= method, which sets the name. If it does, it calls that method.
  3. Since our class uses the attr_accessor :name directive, the name= method is present.  Hence it calls the setter method, setting the value 'Akshay' for the name attribute.
  4. For attributes not defined via attr_accessor , attr_writer or via overridden methods, it raises the UnknownAttributeError.

That's nice, but how can you create new objects?

On its own, the AttributeAssignment module won't allow you to call the new method to create new objects. For that, you need the ActiveModel::API module.

The API module includes the AttributeAssignment module and provides an initialize method. This method takes an attributes hash as argument and calls the assign_attributes method, passing the hash.

module ActiveModel
  module API
    include ActiveModel::AttributeAssignment
    
    def initialize(attributes = {})
      assign_attributes(attributes) if attributes
      super()
    end
  end
end

If you include the ActiveModel::API module in your plain Ruby classes and remove the constructor, you can create new instances like Active Record models.

class Person
  include ActiveModel::API
  
  attr_accessor :name, :age
end

# akshay = Person.new(name: 'Akshay', age: 31)
=> #<Person:0x000000010d045b60 @age=31, @name="Akshay">

How Do Rails Models Get This Behavior?

All Active Record models inherit from the ApplicationRecord class, which inherits from the ActiveRecord::Base class. The Base class includes the ActiveRecord::AttributeAssignment module. Finally, this module includes the ActiveModel::AttributeAssignment module.

Attribute Assignment in Rails

Now, the ActiveRecord::AttributeAssignment module provides its own internal implementation for the private methods, but that's a topic for another blog post.

I hope this post helped you gain a deeper understanding of the AttributeAssignment module. You can use it with plain Ruby classes to simplify your code. You can even use this feature outside Rails by only using the ActiveModel framework and including the ActiveModel::API module.

If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.