Jake Worth

Jake Worth

PostgreSQL Polymorphism

Published: May 30, 2022 • Updated: June 22, 2023 2 min read

  • postgresql

Scenario: you have a database record that can belong to one record or another, but not both. That ambiguous ownership could be called polymorphic.

But maybe you don’t like the polymorphism provided by your framework.

Maybe something about polymorphism at the framework layer doesn’t sit right with you.

Maybe you want database integrity built into your polymorphic implementation.

One solution to this problem is to create a polymorphic-style association at the data layer. By doing so, you’ll get data integrity built in, rather than trusting it will be enforced by each tenant at the application layer.

Here’s how I’ve solved this problem in the past.

The Database Migration

This is a PostgreSQL example, but it could be applied to other databases.

Consider a table called packages. Each package could belong to a factory or a store via a factory_id or store_id column. The real situation this describes is that our physical packages (boxes of items we’re shipping to customers or are being returned) can only be in the factory or the store at any given time.

We can enforce this relationship via the following:

alter table packages
  add constraint check_exclusive_ownership
    check (
      (
        (factory_id is not null)::integer +
        (store_id is not null)::integer
      ) <> 2
    )

We’ve added a constraint to the packages table that checks that factory_id’s truthiness, coerced to an integer (1 for true, 0 for false), added to store_id’s truthiness coerced to an integer, does not equal two. In short, both columns cannot have a not-null value.

Alternately, here’s the same idea with a comparison function (thanks to Reddit user truilus):

alter table packages
  add constraint check_exclusive_ownership
  check (num_nonnulls(factory_id, store_id) <> 2);

If a package must have either a factory or store, even better. Change the constraint to check that the value returned from the parentheses equals one.

alter table packages
  add constraint check_exclusive_ownership
  check (num_nonnulls(factory_id, store_id) = 1);

Why Not Use the Application Layer?

Couldn’t we trust our application to handle this logic? Why bother with a database solution?

To that, I’d say: why not both? Enforcing this logic at the application layer lets the application recover itself gracefully, show error message to the users, and more. And it protects the data from any new tenant that doesn’t yet know this particular business rule.

I’d add a validation to my model that duplicates this logic (Ruby on Rails example):

# app/models/package.rb

validate :exclusive_ownership

def exclusive_ownership
  if factory && store
    errors.add(:base, 'Cannot belong to a factory and a store')
  end
end

Conclusion

Thanks to Jack Christensen, who taught me this technique years ago. Jack and I have been teaching database workshops together since 2016 and I continue to be impressed by his SQL solutions.

What are your thoughts on business logic at the data layer? Let me know!


Join 100+ engineers who subscribe for advice, commentary, and technical deep-dives into the world of software.