I’ve built and implemented a bunch of stuff I’m excited about with the forest, but this post will focus on one aspect: reverse polymorphism. I don’t have it working perfectly yet (EVEN THOUGH IT SHOULD BE WORKING WHAT THE HECK) but I think it is the solution to the craziness I had been encountering with the ActiveRecord associations I need. The best part about it is that it’s not Single Table Inheritance (STI), another potential “solution”.
I’ll describe the problem that needed solving, and how this solves it, and why it’s awesome. Then I’ll describe what’s not quite working yet, the workarounds I’m using, and plea for help from those more knowledgeable.
First, though, here is the gist which introduced me to RP in Rails. I’ll explain how it works in a bit.
The forest’s fundamental blocks are Location objects. They’re normal AR objects. Locations have_many objects, and objects can be multiple kinds of things. The toy examples in the app right now are Wolves and Trees. Wolves can move around, and Trees can’t, but both have_one location.
Ideally, I’d like to be able to call
Location#objects and receive a list (an
ActiveRecord::Relation) that looks like
[wolf1, wolf2, tree1]. I’d like this to work at the ORM level (i.e. I want ActiveRecord to do it for me) and not have to write that scope or method myself.
Polymorphism to the rescue
This sounds like a job for polymorphism, but it’s not a classic case. The normal case is an object being able to belong_to one of multiple kinds of things. I need an object to be able to have_many of many types of things.
It took me a day to grok this, but the solution is a join table. In my case, a LocationObject.
- Location has_many LocationObjects
- LocationObjects (the join table) have a polymorphic have_one association with Wolves, Trees, etc.
- Wolves and Trees have_one LocationObject as object
- Location has_many Wolves and Trees through LocationObjects.
So the associations are all set up.
Someone might say “well Vincent, this is totally more complicated than Single Table Inheritance!” and I would say “ugh come on”. Classical inheritance sounds like a great solution until you see what it does to your database. Even though Wolves and Dogs intuitively could both inherit from a hypothetical Canid class, which inherits from a Mammal class, and so on, this means that everything that inherits from the base class shares the same table. Every time a single child needs a new attribute, it gets added as a column to the ancestor table.
Because the forest will hopefully have a widely-branching tree of life, this is not the proper solution for this project. That single table would probably get huge, and track information for everything from number of leaves to most recent meal. Or whatever. It would be painful.
To keep shared behavior manageable, I’m going to be relying on composing with modules. To continue the previous example, I’d have a Mammal module, and a Canid module, both of which would get mixed in to the lowest-level Wolf and Dog classes. I’m keeping these modules in lib/.
Hell yeah. This seems like the perfect solution.
The problem I’m having is the join.
Wolf#location do not work. I’ve created the following fix, which I really don’t want to be permanent:
# app/models/location.rb ... # FIXME def objects self.location_objects.includes(:object).map do |location_object| location_object.object end end ...
While it DOES avoid a potential N+1 issue, I’m pretty sure ActiveRecord is capable of doing this for me. And as long as this is a pet project, I want the code to be beautiful. I’ve even got failing tests demonstrating this behavior.
Another symptom of this is being unable to create the objects as I’d like.
Wolf.create(name: "joe", location: some_location) just doesn’t work!
It may be the ActiveRecord just doesn’t handle those helper methods over polymorphic join tables. But the writing I’ve seen online doesn’t suggest that.
I have a terrible fear that this is a naming or pluralization issue. ActiveRecord’s errors hint at that:
Could not find the association :location_objects in model Wolf.
Anyways, here’s the relevant code. I invite feedback!