Heading image for post: ActiveRecord to JSON API Part 5: Polymorphism

ActiveRecord to JSON API

ActiveRecord to JSON API Part 5: Polymorphism

Profile picture of Mary Lee

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.

  1. Making the ActiveRecord resources remaining in the application capable of being related to both other ActiveRecord resources, and to JsonApiClient resources
  2. 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:

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!