Polymorphic Namespaced Associations

Rails Active Record with Secondary Read-Only Database

Photo by Jamie Haughton on Unsplash

Let's say there is a really old rails application that has some polymorphic relationships. Now suppose we are writing a new rails application to slowly replace the old one, but we need to access the same database. Besides questioning your life choices, you may run into the same issues I did.

If polymorphic associations are a new concept to you, you probably don't need to be here right now. For some background, you can check out the official documentation on polymorphic relationships which covers 95% of the use cases. Still here? Okay, strap in because this is a bumpy ride.

If you want to look at the code in this example, you can check it out here on Github.

Data Structure

Let's start with the data we are trying to access:

```language-bash
sqlite> select * from humans;
id          name
----------  ----------
1           Kevin
```
```language-bash
sqlite> select * from monkeys;
id          name
----------  -----------
1           Donkey Kong
```
```language-bash
sqlite> select * from comments;
id          value            source_type  source_id
----------  ---------------  -----------  ----------
1           Banana slamma!!  Monkey       1
2           Jim Dandy!       Human        1
```


As you can see we have `humans` and `monkeys` able to make `comments`. The models that created this data look like this:

```language-ruby
class Human < ApplicationRecord
 has_many :comments, as: :source
end
```
```language-ruby
class Monkey < ApplicationRecord
 has_many :comments, as: :source
end
```
```language-ruby
class Comment < ApplicationRecord
  belongs_to :source, polymorphic: true
end
```

The Problem

Everything is jim-dandy until you have another set of namespaced classes trying to access the same data. Those models might look like:

```language-ruby
class Animals::Human < ApplicationRecord
  has_many :comments, as: :source
end
```

```language-ruby
class Animals::Monkey < ApplicationRecord
  has_many :comments, as: :source
end
```

```language-ruby
class Animals::Comment < ApplicationRecord
  belongs_to :source, polymorphic: true
end
```

‍Everything works perfectly fine if you are going from the polymorphic table to the source tables (unless there is a `through` table mixed in there, which in that case you can use `source_type: ‘Monkey’`).

```language-ruby
Animals::Comment.first.source
#<Monkey id: 1, name: "Donkey Kong">
```

‍But if you try to go the other way from the source table to the polymorphic, you get nothing.

```language-ruby
Animals::Monkey.first.comments
```

The above code generates the following SQL:

```language-bash
SELECT
  comments.*
FROM
  comments
WHERE
  comments.source_id = 1
  AND comments.source_type = 'Animals::Monkey'
```

This returns no results, as the `source_type` is set to the class name it originated from. Which makes logical sense, but in this illogical world where I need the source_type to stay ‘Monkey’ and still be able to query the data from this namespace model something needs to change.

You might think, oh… can’t you just slap a `source_type: ‘Monkey’` on that monkey/(human) models `has_many :comments` association? Nope, because that isn’t a valid parameter unless it has `:through` on it (and then we are in a very different type of relationship). I even monkey patched it so I could add source_type, and it didn’t read it from there. I might have been able to get it to work, but I realized I was making the code very difficult to understand if someone else came along.


The Solution

This solution overrides the namespaced `name` to remove the namespacing then add it  back where needed. So the namespaced models now look like:

```language-ruby
class Animals::Human < ApplicationRecord
  has_many :comments, as: :source, class_name: 'Animals::Comment'

  def self.name
    'Human'
  end
end
```

```language-ruby
class Animals::Monkey < ApplicationRecord
  has_many :comments, as: :source, class_name: 'Animals::Comment'

  def self.name
    'Monkey'
  end
end
```

On the `has_many` I have added a `class_name: ‘Animals::Comment’`. This is there so the calling model references the correct namespaced model. This would need to happen anywhere we are making calls to a namespaced model.‍Then the `def self.name` overrides the class name that is used for this class with a non-namespaced version so the sql generates properly.

```language-ruby
Animals::Monkey.first.comments
```

Now generates:

```language-bash
SELECT
  comments.*
FROM
  comments
WHERE
  comments.source_id = 1
  AND comments.source_type = 'Monkey'
```

Conclusion

This solution will resolve issues where you need to access a polymorphic association in which the source class name and the stored source type in the database do not line up. Even then, it is only applicable when trying to access the polymorphic data from the source model.

While a cleaner solution to this would be to update all the records to point to the new class name, this isn’t possible when two different systems are accessing the same data (mind you one is read only, so don’t heave too much).

I ended up being stuck on this problem for a little while trying to figure out a decent solution. While this might not be the most eloquent solution, it is fairly straightforward and hopefully easy to consume for other developers looking at the code.