Owing to the superpowers of the Rails ActiveRecord module, developers can sometimes forget that, under the hood, our AR models are just regular Ruby objects. This allows us to make use of standard instance variables on the objects to adapt their behaviour, which can be useful in particular cases. In this post we discuss the technique and give an example of where it could be used.

Introduction

When introducing the Rails framework, many tutorials and articles will (understandably) rush to introduce the behaviours and API of the ActiveRecord::Base class, showing how ActiveRecord::Base subclasses can be used to represent the models in your business domain, offering an automated object-relational mapping for persisting these models to the database. The ActiveRecord::Base class offers a rich API, [1], covering a huge spectrum of behaviours for your data-modelling needs.

Indeed, the depth of this API is sometimes considered a drawback of the Rails active record (AR) implementation. The expansive API is arguably in violation of the single-responsibility principle, and makes it all too easy for developers to fall into the fat-model antipattern, [2], where all the application logic gets inappropriatedly accumulated in AR models, oftentimes culminating in the crytallization of so-called God objects, [4]. Various techniques have been discussed, [2, 3] to help you transition away from this fat-model anitipattern.

What is all-too-easily missed, when considering the funcationality of our AR models, is that they are just regular Ruby objects with embellished behaviours. That means we can use standard Ruby object functions to tailor our model behaviours. In this post we will look at one example which uses object instance variables for the win.

The full code outlined in this blog post can be found on GitHub, [5].

ActiveRecord Domain Models

We consider the yawn-inducing blog application including User and Post classes, which are defined as follows:

    
# Composed of only :id and :name attributes
class User < ActiveRecord::Base
end

# Composed of attributes :id, :user_id (FK), :title and :body
class Post < ActiveRecord::Base
  belongs_to :user

  validates :user, :body, :title, presence: true

  after_create :enrich_body

  def copy
    Post.skip_callback(:create, :after, :enrich_body)
    Post.new(is_copy: true) do |post|
      sleep 1 # Add a sleep to make things more interesting in the multi-threaded case
      post.user = self.user
      post.body = self.body
      post.title = self.title
      post.save!
    end
  ensure
    Post.set_callback(:create, :after, :enrich_body)
  end

  private

  def enrich_body
    self.body += "\nAuthored by #{user.name}"
  end
end
    
  

The User model relies on the standard inheritied ActiveRecord::Base behaviours, with attributes inferred from the users table. The model has only two attributes: id and name, nothing exciting.

The Post model is a bit more interesting. It has an associated User instance, representing the author of the Post. There is some validation, ensuring the presence of all required attributes and there is also an :after_create hook which updates the body of the post to simply add the author's name.

Beyond the standard ActiveRecord behaviours we have also implemented a custom method to allow us to copy a Post instance. If we simply create a new Post instance with the attributes from the original post, we will hit a problem when we come to save the new model, because the :after_create will fire again and the body will get the author's name added for a second time. So to avoid this behaviour our copy method makes use of the skip_callback method on the ActiveRecord::Base API.

So all is good until we try to run this code in a multi-threaded environment (like Puma or Sidekiq) and things start to go wrong. We can recreate the problem in our unit tests for the model:

    
require "test_helper"

class PostTest < ActiveSupport::TestCase
  setup do
    @user = User.create!(name: "Domhnall")
    @attrs = {
      user: @user,
      title: "AR instance variables",
      body: "Who'd have thunk it."
    }
    @post = Post.new(@attrs)
  end

  …

  test "copy should return a new Post object" do
    assert @post.copy.is_a?(Post)
    assert_not_equal @post.copy.id, @post.id
  end

  test "copy should set post body to be equal to the original" do
    assert_equal @post.copy.body, @post.body
  end

  test "copy should be thread-safe" do
    n=2
    (0...n).map do |i|
      Thread.new do
        puts "Thread #{i}"
        post = Post.new({
          user: @user,
          title: "AR instance variables #{i}",
          body: "Who'd have thunk it #{i}."
        })
        copy = post.copy
        puts copy.body
        assert_equal post.body, copy.body
      end
    end.each(&:join)
  end
end
    
  

Whilst the earlier tests are hopefully self-explanatory, the last test may require some discussion. This test will set up n separate threads, will build a new Post within each thread and will then attempt to copy the newly instantiated Post, finally it will assert that the post body is matching for the original and copied post. When we run this test, things blow up as follows:

Unit test failure when trying to execute copy operation in a multi-threaded environment.

The reason for the error is reported as:

    
After create callback :enrich_body has not been defined (ArgumentError)
    
  

The reason for the failure is the fact that :skip_callback is not thread-safe; the fact we are calling this on the Post class rather than a Post instance should probably raise alarm bells. We can verify that the problem is related to thread-safety because any value of n>1 will give rise to the same error, but if n=1 (representing a single thread) the test suite passes without issue.

So how we solve this thread-safety issue?

Solution using regular instance variables

Our solution will be to make use of a simple instance variable, which we can set on the new object instance during the copy operation and which will control whether we should execute the after_create hook. We will present the solution in full first, and then break it down a little:

    
# Composed of attributes :id, :user_id (FK), :title and :body
class Post < ActiveRecord::Base
  attr_accessor :is_copy
  belongs_to :user

  validates :user, :body, :title, presence: true

  after_create :enrich_body, unless: :is_copy

  def copy
    Post.new(is_copy: true) do |post|
      sleep 1
      post.user = self.user
      post.body = self.body
      post.title = self.title
      post.save!
    end
  end

  private

  def enrich_body
    self.body += "\nAuthored by #{user.name}"
  end
end
    
  

We use the attr_accessor method (see [6]) to define a new instance variable, is_copy, available to each Post instance. This is a regular ruby instance variable, distinct from the typical attributes of our ActiveRecord model, which map to database fields and are persisted to the database when the object is saved. By constrast, this instance variable will simply hold state when the object is in memory, and this will not be persisted to the database. But that is precisely what we need in this case.

The is_copy variable will hold a boolean value, and we will use this as a flag to indicate if the after_create hook should be executed:

    
  after_create :enrich_body, unless: :is_copy
    
  

We now have a simple boolean flag which we can set in our copy operation, to indicate that that after_create should not be executed in this particular context. All that remains is for us to set the is_copy flag as part of the copy operation:

    
  def copy
    Post.new(is_copy: true) do |post|
      sleep 1
      post.user = self.user
      post.body = self.body
      post.title = self.title
      post.save!
    end
  end
    
  

Again we only add the sleep call here to make any multi-threading issues more apparent in our testing. With our new implementation the unit tests pass without issue.:

Full set of unit tests pass without error when our copy operation uses instance variable state, rather than skip_callback.

Summary

The Rails ActiveRecord module offers a plethora of functionality for your domain modelling needs, but under the hood our AR models are just regular ruby objects. In some cases, this basic ruby object functionality is all we need to solve the problem in hand.

In this post we have outlined such an example. We examine a multi-threading issue which arises from our use of the ActiveRecord method, skip_callback. The problem can be avoided by replacing this method with the judicious use of a regular instance variable on our ActiveRecord model.

As always, if you have any thoughts on this technique or any feedback on the post please share through the comments.

If you enjoyed this post, and you are interested in web technology and development, be sure to subscribe to our mailing list to receive an email notifications when we publish future posts.

References

  1. Rails docs on ActiveRecord::Base API
  2. Techniques to decompose fat models
  3. Airbrake blog post on the fat model antipattern
  4. Arkency blog post on God objects in Rails
  5. GitHub repo with source code for this blog post
  6. Post explaining use of attr_accessor

Comments

There are no existing comments

Got your own view or feedback? Share it with us below …

×

Subscribe

Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.