Post

A bit of Active Record's magic 🪄

Have you ever wondered how ActiveRecord, the ORM behind Ruby on Rails, functions? ORMs are crucial tools that simplify database interactions in web applications, yet many developers use them without understanding their inner workings.

Why is that? Well, as long as it works and gets the job done there is no good reason to dive into such a complex piece of software, …

am I right gif

Even though I agree on that one, curiosity gets the best of us, and I always used to wonder: How does ActiveRecord manage to define getter/setter methods based on our DB Schema?

This might seem like a basic question, but it’s fundamental to how we use ActiveRecord daily. This “harmless” question is a good one to start understanding how ActiveRecord internals work, and how some of those Ruby metaprogramming features we talked about in the previous post come in handy here too 💪.

🏊‍♂️ into ActiveRecord::Base

You may have observed that models/application_record.rb typically contains minimal code and inherits from the ActiveRecord::Base class. Inside ActiveRecord::Base you’ll see a lot of include and extend calls on different modules that play a role in how ActiveRecord works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
module ActiveRecord
  class Base
    extend ActiveModel::Naming
    extend ActiveSupport::Benchmarkable
    extend ActiveSupport::DescendantsTracker

    extend ConnectionHandling
    extend QueryCache::ClassMethods
    extend Querying
    extend Translation
    extend DynamicMatchers
    extend DelegatedType
    extend Explain
    extend Enum
    extend Delegation::DelegateCache
    extend Aggregations::ClassMethods

    include Core
    include Persistence
    include ReadonlyAttributes
    include ModelSchema
    include Inheritance
    include Scoping
    include Sanitization
    include AttributeAssignment
    include ActiveModel::Conversion
    include Integration
    include Validations
    include CounterCache
    include Attributes
    include Locking::Optimistic
    include Locking::Pessimistic
    include Encryption::EncryptableRecord
    include AttributeMethods
    include Callbacks
    include Timestamp
    include Associations
    include SecurePassword
    include AutosaveAssociation
    include NestedAttributes
    include Transactions
    include TouchLater
    include NoTouching
    include Reflection
    include Serialization
    include Store
    include SecureToken
    include TokenFor
    include SignedId
    include Suppressor
    include Normalization
    include Marshalling::Methods

    #...
  end
end

That’s a lot of modules 😱! We’ll only dive into a couple of them though; after skimming through the code, I found that the getter/setter method definitions based on our DB Schema take place in the ModelSchema and AttributeMethods modules.

AttributeMethods Module

Let’s first dive into the AttributeMethods module and suppose we already know what the columns in our DB tables are. The first couple of things that caught my eye when digging into this module were the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
module ActiveRecord  
  module AttributeMethods
    extend ActiveSupport::Concern
    include ActiveModel::AttributeMethods

    included do
      initialize_generated_modules
      include Read
      include Write
      include BeforeTypeCast
      include Query
      include PrimaryKey
      include TimeZoneConversion
      include Dirty
      include Serialization
    end

    class GeneratedAttributeMethods < Module
      include Mutex_m
    end

    #...

    module ClassMethods
      def initialize_generated_modules
        @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
        private_constant :GeneratedAttributeMethods
        @attribute_methods_generated = false
        @alias_attributes_mass_generated = false
        include @generated_attribute_methods

        super
      end

      #...
    end
  end
end

First of all, this module heavily relies on ActiveModel::AttributeMethods, which gives us methods that you might be familiar with, such as: attribute_method_prefix, alias_attribute, and more.

It’s important to remember that the ActiveModel’s API was extracted from the ActiveRecord component to allow us to create POROs with “model-like” behavior (validations, type casting, etc.).

Then, you can see a bunch of sub-modules included that self-describe their purpose, such as Read (for reader/getter methods), Write (for writer/setter methods), PrimaryKey, Dirty (for tracking changes), etc.

And finally, you can see a method initialize_generated_modules that gets called as soon as the module is included, where it sets up an empty GeneratedAttributeMethods module into the @generated_attribute_methods variable. Inside this GeneratedAttributeMethods module is where all of the getter/setter methods will be defined, and then included in your class.

This pattern of defining an empty module, then dynamically adding methods to it and finally including it is a common approach you’ll see in most of Ruby on Rails codebase.



When reading any Ruby open-source code, looking at the “ClassMethods” module within the module you’re looking at might be helpful, since usually the “API” or “DSL” methods live in there.

But how does it work!?

Now that we know the basic structure, let’s dive into the actual code that defines the getter/setter methods based on our DB Schema.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module ActiveRecord
  module AttributeMethods
    module ClassMethods
      # Generates all the attribute related methods for columns in the database
      def define_attribute_methods
        #...
        super(attribute_names)
      end

      #...

      # Returns an array of column names as strings if it's not an abstract class and
      # table exists. Otherwise it returns an empty array.
      # Person.attribute_names
      # eg: => ["id", "created_at", "updated_at", "name", "age"]
      def attribute_names
        @attribute_names ||= if !abstract_class? && table_exists?
          attribute_types.keys
        else
          []
        end.freeze
      end
    end
  end
end

The super call ends up invoking the define_attribute_methods method that lives within ActiveModel::AttributeMethods, let’s take a look at it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module ActiveModel
  module AttributeMethods
    included do
      class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
    end

    #...

    module ClassMethods
      #...

      def define_attribute_methods(*attr_names)
        #...
        attr_names.flatten.each do |attr_name|
          define_attribute_method(attr_name)
        end
      end

      def define_attribute_method(attr_name)
        attribute_method_patterns.each do |pattern|
          method_name = pattern.method_name(attr_name)
          generate_method = "define_method_#{pattern.proxy_target}"

          send(generate_method, attr_name.to_s, owner: owner)
        end
      end

      #...
    end

    #...
  end
end

So what’s happening here? The ActiveModel::AttributeMethods uses the class_attribute helper inside the included do block which we saw in the previous post, it defines a class attribute where we’ll save the “attribute method patterns”, we’ll come to this later.

Then, we can find the method we were looking for: define_attribute_methods which goes through each attribute name and calls define_attribute_method for each one of them.

This last method goes through each pattern available in the class attribute defined above and dynamically decides what method to call. This “dynamically deciding” will make sense when looking at the AttributeMethodPattern and how it is used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AttributeMethodPattern # :nodoc:
  attr_reader :prefix, :suffix, :proxy_target, :parameters

  def initialize(prefix: "", suffix: "", parameters: nil)
    @prefix = prefix
    @suffix = suffix
    @proxy_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    #..,
  end

  def method_name(attr_name)
    @method_name % attr_name
  end

  #...
end

If we only take into account the default AttributeMethodPattern that was added inside ActiveModel::AttributeMethods with no prefix or suffix, when calling generate_method = "define_method_#{pattern.proxy_target}" it will return "define_method_attribute":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module ActiveModel
  module AttributeMethods
    included do
      class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
    end

    module ClassMethods
      #...

      def define_attribute_method(attr_name)
        attribute_method_patterns.each do |pattern|
          method_name = pattern.method_name(attr_name)
          generate_method = "define_method_#{pattern.proxy_target}"
          # ============================================================
          # ==== generate_method will be "define_method_attribute" =====
          # ============================================================

          send(generate_method, attr_name)
        end
      end
    end
  end
end

So when we use send to call that method, who will answer to define_method_attribute(attr_name)?

The answer is…… 🥁🥁🥁🥁 The ActiveRecord::AttributeMethods::Read module that we mentioned above!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module ActiveRecord
  module AttributeMethods
    # = Active Record Attribute Methods \Read
    module Read
      extend ActiveSupport::Concern

      module ClassMethods # :nodoc:
        private

        def define_method_attribute(name, owner:)
          "def #{name}" <<
          "  _read_attribute(#{name}) { |n| missing_attribute(n, caller) }" <<
          "end"
        end
      end

      #...

      def _read_attribute(attr_name, &block)
        #...
      end
    end
  end
end

And there we have getter/reader methods defined for each attribute! But what about the setter/writer methods 🤔? It’s as simple as adding a new attribute_method_pattern 😃! Let’s look at the ActiveRecord::AttributeMethods::Write module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module ActiveRecord
  module AttributeMethods
    module Write
      extend ActiveSupport::Concern

      included do
        attribute_method_suffix "=", parameters: "value"
      end

      module ClassMethods
        private

        def define_method_attribute=(name)
          "def #{name}(value)" <<
          "  _write_attribute(#{name}, value)" <<
          "end"
        end
      end

      def _write_attribute(attr_name, value)
        #...
      end
    end
  end
end

By adding a new attribute_method_pattern with a suffix of = ActiveModel::AttributeMethods will call define_method_attribute= when iterating over that specific method pattern!

I encourage you to look at the other sub-modules inside ActiveRecord::AttributeMethods since you’ll see they follow the same pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module ActiveRecord
  module AttributeMethods
    included do
      initialize_generated_modules
      include Read
      include Write
      include BeforeTypeCast
      include Query
      include PrimaryKey
      include TimeZoneConversion
      include Dirty
      include Serialization
    end
  end
end

When do the getter/setter attribute definitions take place?

If you paid close attention, you may have noticed that we never showed who calls the initial define_attribute_methods method that starts the whole process.

Here’s where Railties comes into play, Railties “ties” all of the Rails components together, and it’s responsible for orchestrating the “initialization” of a Rails application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module ActiveRecord
  class Railtie < Rails::Railtie 
    initializer "active_record.define_attribute_methods" do |app|
      config.after_initialize do
        #...

        ActiveSupport.on_load(:active_record) do
          descendants.each do |model|
            model.define_attribute_methods
          end
        end
      end
    end
  end
end

And there you have it! We have now defined all of the getter and setter attributes for our models 💪!

Quick 🏊‍♂️ into ActiveRecord::ModelSchema

So far we supposed we already knew all of the columns in our DB tables, but we’ll quickly unravel how this happens under the hood. Let’s take a quick look at the load_schema! method inside ActiveRecord::ModelSchema:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module ActiveRecord
  module ModelSchema
    #...

    module ClassMethods
      def load_schema!
        columns_hash = connection.schema_cache.columns_hash(table_name)
        # ...
      end

      #...
    end
  end
end

It’s pretty clear that based on a db connection and a table name it will fetch all of the columns in it. At first, I thought loading the schema would involve reading the famous db/schema.rb file, but it turns out the schema is loaded by querying the DB itself! For instance, this is how that query looks like for MySQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
      #...

      def column_definitions(table_name)
        execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
          each_hash(result)
        end
      end

      #...
    end
  end
end

Conclusion

And that brings us full circle! We’ve explored how ActiveRecord defines getter and setter methods aligned with our database schema and how it dynamically loads this schema.

Ruby on Rails codebase is an awesome place to learn more about Ruby metaprogramming features and how to use it to your advantage; I hope this post has given you a glimpse of that! 🎉

This post is licensed under CC BY 4.0 by the author.