Rails developers: avoid has_one
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:
And here’s what you should do, instead:
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.
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:
- ALTER TABLE [parent table] ADD [all columns in child table]
- UPDATE [parent table] SET [column1 of child table] = (SELECT column1 FROM [child table] WHERE [child table].[foreign key] = [parent table].id, …
- DROP TABLE [child table]
You won’t regret it! Now go forth and make the world simpler.