ActiveRecord to JSON API Part 6: Mimicking AR


This post is by Hashrocket from Hashrocket


Click here to view on the original site: Original Post




As we worked through the conversion from ActiveRecord to JsonApiClient, we came across some methods implemented by ActiveRecord that would still be useful to have around.

Background

We were building an internal JSON API using the following libraries:

For more information on our goals and set up for the internal API, see part 1 of this series.

Pluck

One of the first things we noticed we were converting over and over again was ActiveRecord’s pluck method. With the JsonApiClient resources, we were converting all the ActiveRecord pluck calls to chaining select(:field).map(&:field).

Not only was this repetitive and time consuming to convert, but we felt like it was burdensome for future developers on the application to have to remember to do.

We decided to try to figure out how we could implement pluck on the resources. We knew from the documentation that we could create a custom query builder for our resources, so we decided to try that route.

We knew we wanted to imitate the behavior of pluck, so if one field was requested, a flat array was returned, and if multiple fields were requested, nested arrays were returned.

class CustomQueryBuilder < JsonApiClient::Query::Builder
  def pluck(*fields)
    result = select(fields).map do |record|
      record.attributes.slice(*fields).values
    end

    (fields.size == 1) ? result.flatten : result
  end
end

With the custom query builder in place, we just had to specify to the base JsonApiClient resource to use it.

class BaseResource < JsonApiClient::Resource
  # …
  self.query_builder = CustomQueryBuilder
  # … 
end

After updating the base JsonApiClient resource, we were now able to use pluck.

Foo.pluck(:id) # => [ 1, 2, 3, … ]
Foo.where(fizz: "buzz").pluck(:id) # => [ 2, 5, … ]

Total Count

Another important method from ActiveRecord that we found ourselves having to replace was count. In theory, we could get all resources and just call count as usual, but we’d then be returning all resources from the API for the sole purposes of getting a count, which was an extremely resource heavy way to go.

Our first step was to figure out how to get a total count of resources without returning everything from the API. Because we had figured out pagination, and we knew that Kaminari was in some way getting a total count, we figured we could piggy back off that to get our count without fetching a large number of resources.

Our compromise was to fetch only one resource, specifying our page as page 1, and our page sizes also as 1. From there, we could call total_count on the paginated object to get the full count. We were still returning a full object that we did not need, but at least it was only one.

Foo.where(fizz: "buzz").per(1).page(1).total_count # => 23

This was a fairly performant way to get total counts, but we again felt like it was rather burdensome for future developers to remember and maintain.

Since we already had our custom query builder in place, we decided to add a count method.

class CustomQueryBuilder < JsonApiClient::Query::Builder
  # … 

  def count
    per(1).page(1).total_count
  end
end

Now we could append count to the end of our queries without the performance impact.

Foo.where(fizz: "buzz").count # => 23

Find By

The third ActiveRecord method we found ourselves replacing throughout the application was find_by. Since find_by was a class method, and not something we were appending to an existing query, we didn’t have to use the custom query builder to implement it for our JsonApiClient resources. We only had to add it to our base JsonApiClient resource.

class BaseResource < JsonApiClient::Resource
  # …
  def self.find_by(params = {})
    where(params).first
  end
  # … 
end

With that change, we were able to continue using find_by.

Closing Thoughts

While the count method we implemented was mainly for performance purposes, the pluck and find_by were simply convenience methods meant to make it easier on future developers, as well as to reduce our workload during the conversion.

Check out the rest of this series!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.