ActiveRecord to JSON API Part 6: Mimicking AR
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:
- json_api_client for reading data from the API
- jsonapi-resources for creating the internal JSON API endpoints
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 JsonApiClient 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!