TL;DR has_many
is an anti-pattern which leads straight to monolithic applications.
First - what defines a monolithic application? It is one that cannot be split apart because everything depends on everything else. No part can be extracted to an external library - the library would have to be the size of our entire app. Let this be our working definition of a monolith. Ignore, for the moment, whether it's deployed on more than one machine, but ask could it be separated if you desired.
One major culprit of not being able to split an application up is a dependency cycle.
Why are dependency cycles so bad? Because when there is a dependency cycle, every module is ultimately dependent on every other— the monolith problem. So, what explains the persistence of dependency cycles in almost every Rails app, ever?
has_many
. It has seduced you with it's allure. First it woos you with its eerily easy way of getting to child entities - user.posts
, customer.orders
, etc. Then it distracts you with its apparent complementary nature with belongs_to
. You can't split up this dynamic duo, like peanut butter & jelly, right? Wrong. has_many
is the Siren that lured your ship straight into the Cliffs of Monolith.
What is the first model you added to your application? Probably User, right? So, once you wrote user.rb
and its corresponding tests, and committed it - why did you ever open that file up again to tell it about something that it did not need to know existed? Rails keeps you from reopening user.rb
if you add a column to the User table, and this is good, right? So why, when you added a Posts table far away, did you open up User again to make it aware of Posts? Did the definition of being a user change? Did you did not realize you were violating the Open-Closed Principle, one of the 5 principles of SOLID design? Somewhere inside I bet you knew it felt dirty to keep opening up User and making it aware of things that it had been blissfully unaware of. But you did it anyway, and so did I for a long time, but it's possible to quit.
belongs_to
, on the other hand, is a SOLID, dependable creature like a loyal Saint Bernard. Its necessity follows from the need to enforce the rule that a Post must never exist without a User. Post is described as being "functionally dependent" on User, in the language of databases. It is a smell, in a relational database model, for a parent table know about its children. Except in strategic cases like creating a denormalized post_count
field on User. Boyce and Codd, the seriously smart and analytical creators of the Relational Model defined the degrees of normalization to include 3rd Normal Form and even a stronger 5th normal form, with the recommendation that we at least achieve 3NF. This is good guidance to heed.
Now, some seriously smart people designed Rails as well, but nobody could have predicted how long-lived Rails applications would have become, way back in 2005. And thus we're in the predicament we're in today. has_many
is established practice, yet harmful. It is mostly to blame for user.rb
having the worst thrash in your codebase. It has turned your data model into a web of dependency cycles.
But now - the easy, rewarding part - seeing that doing without has_many
will impact you less than you once thought. Which of the following will reduce thrash of the user.rb
file, allow you to follow SOLID principles, and break dependency cycles in your application?
posts = user.posts
posts = Posts.for(user)
Now that you know, it's obvious. And the implementation of for
, while not done yet, would be trivially simple. I hope to see an implementation of for
soon, possibly even with deprecation warnings on has_many
.
Happy Coding!
<script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-59977321-1', 'auto'); ga('send', 'pageview'); </script>
for
method I described, it's a change I think needs to be written.has_many
explains all monoliths. Just that for this criteria of being a monolith - having a dependency cycle -has_many
is to blame. If other, measurable criteria exist, let me know, I'll write about those too 😀Though I agree with it, I couldn't write as strongly about the Single Responsibility Principle, though perhaps I should. Every app I've ever worked on had an average responsibility per module greater than 1.0 - to itself, and to at least one other.