ActiveRecord to JSON API Part 5: Polymorphism
The Rails application we were working with had a number of polymorphic relationships that included the models we were moving to the JSON API, as well as a polymorphic relationship within the models being migrated.
This raised an interesting question for us. How do we maintain those relationships without changing the core functionality of the app and the models involved? And how will the polymorphic API associations function?
We wound up facing two issues with polymorphism during the conversion process.
- Making the ActiveRecord resources remaining in the application capable of being related to both other ActiveRecord resources, and to JsonApiClient resources
- Making JsonApiClient resources fetch polymorphic API associations when the associated data was not eager loaded from the API.
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.
Making ActiveRecord Work with JsonApiClient Resources
There were a number of models in the application we were working on that had polymorphic relationships with models that were remaining in the application, as well as models that were being pulled out for the microservice. We needed to figure out how to make those polymorphic relationships continue to work.
The least complicated way we could think of was to remove the ActiveRecord belongs_to
and manually define the setter and getter methods for the polymorphic relationship.
So something that originally looked like this:
class SystemChange < ApplicationRecord
belongs_to :changeable, polymorphic: true
end
Turned into this:
class SystemChange < ApplicationRecord
def changeable
@changeable ||= changeable_type.constantize.where(id: changeable_id).first
end
def changeable=(changeable)
self.changeable_id = changeable.id
self.changeable_type = changeable.class.to_s
end
end
With those changes, our SystemChange
model could be related to both ActiveRecord models and JsonApiClient resources.
While it worked, it also meant that any joins we did had to be manually defined, since the relationship was no longer set up in ActiveRecord. We determined this was a worthwhile tradeoff.
Fetching Polymorphic Associations with JsonApiClient
Part of the models being moved to our API included a model with a polymorphic relationship to the other API models. The JSONAPI::Resource library fully supported polymorphic relationships, which was a huge help for us.
# JSONAPI Resource
module LocalAPI
class FooResource < BaseResource
has_one :bar, polymorphic: true
end
end
As long as the model backing our JSONAPI::Resource had the polymorphic relationship defined, everything was good to go.
The problem for us came in on the other side: the JsonApiClient didn’t seem to have anything in place to handle polymorphic relationships. We knew we could manually define the relationship within the resource.
# JsonApiClient Resource
class Foo < BaseAPIResource
property :bar_id, type: :string
property :bar_type, type: :string
def bar
bar_type.constantize.where(id: bar_id).first
end
end
However, a manually defined relationship wouldn’t help us with eager loading the data, which was a big deal for the resource we were working with. So we defined the relationship, just to see what would happen.
# JsonApiClient Resource
class Foo < BaseAPIResource
property :bar_id, type: :string
property :bar_type, type: :string
has_one :bar
# def bar
# bar_type.constantize.where(id: bar_id).first
# end
end
As it turns out, if we eager loaded bar
, the relationship worked! However, if we didn’t eager load bar
, the JsonApiClient tried to look for another JsonApiClient resource called Bar
, which obviously wasn’t defined, because it was polymorphic.
# Eager loaded resources
foo = Foo.includes(:bar).where(id: 123).first
foo.bar # => returned the associated resource
# Fetched resources
foo = Foo.where(id: 123).first
foo.bar # => NameError
This was actually a best case scenario behavior-wise for us, since eager loading was the most important piece we needed. But how do we create a getter that says to use the preloaded resource if it’s available, otherwise manually fetch? Turns out, we could check foo
relationship attributes, looking for a key called “bar” and within that, a key called “data”. If data was present, that meant that the association was preloaded, and we could let the JsonApiClient library do its thing. If the data wasn’t present, then we could manually specify how to fetch the relationship.
# JsonApiClient Resource
class Foo < BaseAPIResource
property :bar_id, type: :string
property :bar_type, type: :string
has_one :bar
def bar
if relationships.attributes.dig("bar", "data").present?
super
else
bar_type.constantize.where(id: bar_id).first
end
end
end
Now, we had everything working!
# Eager loaded resources
foo = Foo.includes(:bar).where(id: 123).first
foo.bar # => returned the eager loaded associated resource
# Fetched resources
foo = Foo.where(id: 123).first
foo.bar # => returned the fetched associated resource
Closing Thoughts
The JSONAPI::Resources library handled polymorphism perfectly, which was awesome. Figuring out how to make the JsonApiClient library also handle polymorphism was a little more troublesome, but we got it working.
Check out the rest of this series!