Graceful handling of missing STI constants

When using STI, if you introduce a new subclass in a git branch, then create a db record while in it, then switch to another branch, you will get uninitialized constant error when trying to query those records. Certain kinds of applications (like ours) could run into this a lot, so I’m wondering if there could be a null-object-patternish solution to this. E.g:

  • Accept proc for inheritance_column, which evaluates on instances, with type passed as an arg to it, and whatever it returns becoming the record in that case. This way we can plug those records with some placeholders

  • Add option self.ignore_unknown_types = true to activerecord to simply auto-rescue + skip records with uninitialized constant error (perhaps print a warning instead)

Only to be used in dev/staging of course. Any thoughts?

Why not just use different databases in your different git branches?

Why not just use different databases in your different git branches?

To avoid losing accumulated sample data, and avoid maintaining setup scripts when they aren’t a priority yet.

When using STI, if you introduce a new subclass in a git branch, then create a db record while in it, then switch to another branch, you will get uninitialized constant error when trying to query those records. Certain kinds of applications (like ours) could run into this a lot, so I'm wondering if there could be a null-object-patternish solution to this. E.g:

* Accept proc for inheritance_column, which evaluates on instances, with type passed as an arg to it, and whatever it returns becoming the record in that case. This way we can plug those records with some placeholders

The proc can’t be run on an instance - the error is arising when AR tries to determine what class to use. The calling context is in find_sti_class:

https://github.com/rails/rails/blob/6d72485b6977804a872dcfa5d7ade2cec74c768e/activerecord/lib/active_record/inheritance.rb#L169

a class method defined on ActiveRecord::Base. At runtime, this will have access to the class that `find` was called on (presumably the STI base class) and the value from the inheritance column.

Returning a fully-initialized object from here would require skipping a bunch of code in `instantiate`.

* Add option self.ignore_unknown_types = true to activerecord to simply auto-rescue + skip records with uninitialized constant error (perhaps print a warning instead)

It’s not clear to me what “skipping” a record in this scenario would mean. If I have a model that belongs_to an instance of the missing class, what do I get back when I access it? `nil`? `RecordNotFound` exception? If I have a validation that requires the presence of that missing object, does `save` still work?

There are plenty more questions like those. “Muddling through” with bad data is almost certainly going to break something upstream in people’s applications - something considerably harder to troubleshoot than a straightforward `SubclassNotFound` error on loading a bad record.

As to the “sample data” issue, if it’s worth keeping in the DB it’s worth keeping in source control. If writing a db/seeds.rb file is too complicated (maybe you’re importing large datasets from elsewhere), I’d recommend things like YamlDb to make repeatable seed data. Even `mysqldump > sample_data.sql` is a start…

—Matt Jones

When using STI, if you introduce a new subclass in a git branch, then create a db record while in it, then switch to another branch, you will get uninitialized constant error when trying to query those records. Certain kinds of applications (like ours) could run into this a lot, so I’m wondering if there could be a null-object-patternish solution to this. E.g:

  • Accept proc for inheritance_column, which evaluates on instances, with type passed as an arg to it, and whatever it returns becoming the record in that case. This way we can plug those records with some placeholders

The proc can’t be run on an instance - the error is arising when AR tries to determine what class to use. The calling context is in find_sti_class:

Sorry I accidentally left that confusing “evaluates on instances” in there, noticed after hitting send, I meant everything besides that; i.e. it gets passed the type value, no model instances involved.

  • Add option self.ignore_unknown_types = true to activerecord to simply auto-rescue + skip records with uninitialized constant error (perhaps print a warning instead)

It’s not clear to me what “skipping” a record in this scenario would mean. If I have a model that belongs_to an instance of the missing class, what do I get back when I access it? nil? RecordNotFound exception? If I have a validation that requires the presence of that missing object, does save still work?

There are plenty more questions like those. “Muddling through” with bad data is almost certainly going to break something upstream in people’s applications - something considerably harder to troubleshoot than a straightforward SubclassNotFound error on loading a bad record.

While you’re right about this being a can of worms, I was trying to think of a way to piggyback on existing fully defined behavior, like “pretend record is not in database, and everything that implies. End of story”

As to the “sample data” issue, if it’s worth keeping in the DB it’s worth keeping in source control. If writing a db/seeds.rb file is too complicated (maybe you’re importing large datasets from elsewhere), I’d recommend things like YamlDb to make repeatable seed data. Even mysqldump > sample_data.sql is a start…

We have very image-heavy application, and it’s not ideal to have big payloads of files dragged around in the repo, but it’s not the end of the world either. There are certainly all kinds of workarounds, but this feature seemed reasonable to bring up, so here I am.

You're facing one possible way to have incompatible schemas between
branches.

They are created by workflows external to Active Record, to me it doesn't
makes sense that Active Record acts in a way it is not supposed to, to
address divergences in Git branches.

Divergence in Git branches is an external issue, and should be solved where
it is originated in my view, at the workflow level.

If your application has a very specific use case for STI it could be the
case that your most efficient way to workaround it is by monkey patching
something in AR... that depends on the details. But certainly I don't see
the solution builtin in AR.

If I were you I’d stub out all the different objects with ‘shell’ implementations in your master branch, and then AR won’t blow up when it tries to instantiate the objects. Then you can leave implementation details to the branches where you want to implement/test them.

Also it seems odd to me that you have a workflow that consistently creates new objects that will rely on STI implementations. Are all those objects different from eachother? (That is, do they each implement a certain domain logic that can best be implemented with Ruby code). If the answer is no, I’d question whether or not STI is the right pattern for what you are doing.

-Jason

If I were you I’d stub out all the different objects with ‘shell’ implementations in your master branch, and then AR won’t blow up when it tries to instantiate the objects. Then you can leave implementation details to the branches where you want to implement/test them.

Yeah, this is one of a few possible workarounds, but very inconvenient for us.

Are all those objects different from eachother? (That is, do they each implement a certain domain logic that can best be implemented with Ruby code).

Yes, that’s exactly the case.