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.
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
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!