Rails developers: avoid has_one

Adam Hooper
3 min readApr 1, 2016

--

I’ve been struggling with Other People’s Code and Other People’s Guidance.

My beef today is has_one. Quoth the Active Record Associations Guide:

For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier.

Don’t think this way. It’s a recipe for complexity.

Here’s what the guide suggests:

Complicated code with complicated edge cases

And here’s what you should do, instead:

Less code with fewer edge cases

This is less code. It results in fewer SQL statements. It executes faster. It avoids race conditions. It’s more maintainable.

Why, oh why, would you throw an extra database table at your problem?

Here’s why people consider using two tables instead of one:

Because an account is distinct from a supplier

You’re writing code, not English. You do understand the difference. I know you do, because you’re reading this article instead of warning your suppliers there’s an active record-base right beside them that’s greater than they are.

Close your eyes and repeat the mantra: simpler code is more reliable and less work.

In this example, you should absolutely put your account number in the suppliers table.

Because “belongs to” sounds backwards

Hey, that’s what the Active Record Associations guide says!

Don’t trust English. Trust code. Here’s all you need to know: “belongs_to” is good; “has_one” is bad.

Because I may eventually have many supplier accounts

That’s fine. When you do, write a migration.

In the meantime, code for today’s requirements. You won’t regret it.

Because writing to a separate table makes writes faster

Perhaps you plan on updating the “accounts” table frequently and the “supplier” table rarely. You’ve read that a database UPDATE amounts to writing a new row and deleting an old one, and you know your “supplier” table has a huge TEXT blob in it. You don’t want to update that big table every time there’s a small tweak to the “accounts” table.

You’re overthinking it. (And you’re wrong: a TEXT column is a pointer, so an UPDATE on the supplier table is quick.)

Don’t optimize prematurely. Find the real bottlenecks and fix those.

Because writing big data to a separate table makes SELECT faster

ActiveRecord indeed takes a long time to pull in TEXT and BLOB values, and you often don’t want to load them with every request.

Use ActiveRecord::Base.select() when you want to avoid sending those values over the wire. If the “supplier” table has a big column that makes this query slow:

suppliers = Supplier.order(:id).limit(10)

Then optimize by querying for everything other than that column:

supplier = Supplier.order(:id).limit(10).select(Supplier.column_names - [ 'big_column' ])

Because locks. Because transactions. Because optimistic concurrency.

I don’t exactly understand what feature you’re trying to code. I suggest you open your mind to the possibility that has_one will not help you achieve what you want.

has_one

Because I’m using has_one :through

This post isn’t about has_one :through. That’s a totally different relation. (Rails developers should have called it has_one_through() — it makes no sense that the same method name does two completely different things.)

Because I want a polymorphic association

Okay, you got me. You may actually want has_one.

Think before you leap, though. Polymorphic associations are extremely complicated. You can usually avoid them; and if you can avoid complexity, you should.

Because my code already uses has_one, and changing it is hard

Here’s how to migrate from has_one to a single table:

  1. ALTER TABLE [parent table] ADD [all columns in child table]
  2. UPDATE [parent table] SET [column1 of child table] = (SELECT column1 FROM [child table] WHERE [child table].[foreign key] = [parent table].id, …
  3. DROP TABLE [child table]

You won’t regret it! Now go forth and make the world simpler.

--

--

Adam Hooper
Adam Hooper

Written by Adam Hooper

Journalist, ex software engineer

Responses (2)