Edit 2016-04-08: Lessons learned after 48 hours
So I got more feedback to this than I anticipated, mainly via reddit.
Some in the predictable “you’re ugly” form, but a lot of genuinely good suggestions on how to avoid / go around some of the pain points I describe in this post.
I think the main thing was that several people pointed out that it’s completely possible to have your models as POROs, and then use ActiveRecord (or alternatively, something like Sequel) for data access only.
To be perfectly honest, I’m not 100% sure what that would look like, and whether it would be as convenient as I’d like, but it would definitely be an improvement over sticking everything in an
This solution would also greatly help with the issue I have with unit testing, enabling me to decouple those from the database, and definitely cut down on their runtime.
In conclusion – I wouldn’t say that rails is terrible / should not be used.
I would definitely say that it has some big traps which are harder to avoid than I’d like.
If I had to re-write this post today, I’d definitely change its title to “Some of Rails’s biggest gotchas”, and I wouldn’t say that you should categorically not use it.
You just need to use it carefully.
Thanks for reading, and for teaching me quite a few new things.
Here’s the original post:
Lately I’ve had the chance to work on a large-ish server application written in RoR. I went from “Wow look at all these cool conventions, I know exactly where everything needs to go!” to “err.. how the fuck do I scale this?” and “This is not where this should go!” in 8 weeks. And this is (partly) why:
(* Yes, this post’s title is a total clickbait. I don’t actually hate Rails; I just think it promotes a lot of bad practices.)
I’ve never personally used the ActiveRecord pattern in previous projects; I always felt this would mix up domain concerns with persistence concerns, and create a bit of a mess.
Well guess what, I was right.
In the specific project I was working on, the code for domain classes would typically consist of 70% business logic, and 30% stuff to do with DB access (scopes, usually, as well as querying / fetching strategies).
That in itself is a pretty big warning sign that one class is doing too much.
The arguments as to why ActiveRecord in general is a bad idea are well documented; I’ll briefly recap here:
It breaks the Single Responsibility Principle
A model class’s responsibility is to encapsulate business rules and logic.
It’s not responsible for communicating with data storage.
As I mentioned before, a considerable amount of code in our project’s domain classes was dedicated to things like querying, which are not business logic.
This causes domain classes to be bloated, and hard to decouple.
It breaks the Interface Segregation Principle
Have you ever, while debugging, listed the methods of one of your domain objects? Were you able to find the ones that you defined yourself? I wasn’t.
Because they’re buried somewhere underneath endless
ActiveRecord::Basemethods such as
Well, I made up a few there, but
ActiveRecord::Baseinstances have over 100 methods, most of them public.
ISP tells us that we should aspire to have small, easy-to-understand interfaces for our classes. Dozens of public methods on my classes is another indication that they’re doing waaaay too much.
Its database abstraction is leaky
Abstracting-away the database is notoriously hard. And I believe that ActiveRecord doesn’t do a particularly good job of this.
As noted before,
ActiveRecord::Basepollutes your public interface with a plethora of storage-related methods. This makes it very easy for a developer to make the mistake of using one of these very storage-specific methods (i.e
column_for_attribute) inside a controller action, for example.
update_attributeindicate that the using code knows a little too much about the underlying persistence layer.
If there was one thing I knew about Rails before having written a single line of ruby code, it was that everything in Rails is unit-tested. Rails promotes good testing practices. Hooray!
So, obviously, one of the first things I read about concerning RoR development, was how to test:
Testing support was woven into the Rails fabric from the beginning.
Right on! Finally, somebody gets it right!
Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you’ll need to understand how to set up this database and populate it with sample data.
I must have misread this.. let me check again..
your tests will need a database
Yup. I need a bloody database to test my business logic.
So why is this so bad?
The meaning and intent behind unit tests is to test single units of code.
That means that if the test fails, there can only be one reason for it- the unit under test is broken.
This is why you fake everything external that the unit under test interacts with; you don’t want a bug in a dependency to cause your current unit test to fail.
For example, when testing the
total_salaries method, you use fake
Employee objects, with a fake
salary property, which would return a predefined value.
That way, if we get a wrong
total_salaries value, we’ll know for sure that the problem lies within the
Payroll class, and nowhere else.
But, with rails testing, you’re not encouraged to fake anything.
That way, if the
total_salaries test fails, it can be because
Employee is broken, or my database schema is wrong, or my database server is down, or even something as obscure as a child object of
Employee has some required attribute missing, so it can’t be persisted, and an error is thrown.
This is not how a unit test is supposed to work.
Not only does Rails encourage you to write non-unit unit tests, it also makes it nearly impossible, and very dangerous to go around it and write proper unit tests.
(* Note that if you use hooks, such as
before_update etc., it becomes even more horrible)
Apart from the horribleness of making it harder for me to determine what went wrong when a test fails, this complete abomination of a testing strategy caused me some more hair-tearing moments:
- Our unit test suite took 18 minutes to run. Even when using a local, in-memory database.
- My tests failed because the database wasn’t initialized properly.
- My tests failed because the database wasn’t cleaned properly by previous tests (WTF?).
- My tests failed because a mail template was defined incorrectly.
Since sending an email was invoked in a
before_createcallback, failing to send an email caused the callback to fail, which caused
createto fail, which meant that the record was not persisted, which meant that my test was fucked.
Too much magic
Magic is something that is inherent to any framework; by definition, if it does some sort of heavy lifting for you, some of it is going to be hidden from your view.
That’s doubly true for a framework which prefers “convention over configuration”,
meaning- if you name your class / method the correct way, it’s going to be “magically” wired up for you.
This kind of magic is fine and acceptable. The magic that I have a problem with is rails’ extensive usage of hooks (aka callbacks); Be it in the controller (before / after action), or in the model (before / after create / update / delete…).
Using callbacks immediately makes your code harder to read:
With regular methods, it’s easy to determine when your code is being executed – you can see the method being called right there in your code.
With callbacks, it’s not obvious when your code is being invoked, and by whom.
I’ve had several instances of scratching my head, trying to figure out why a certain instance variable was initialized for one controller action and not for another, only to track it down to a problem with a
before_action callback of a parent class.
The fact that ActiveRecord callbacks can’t be turned off is also a pain in the ass when testing, as I described previously.
Additionally, callbacks are, of course, very hard to test, since their logic is so tightly coupled to other things in the model, and you can’t trigger them in isolation, but only as a side effect of another action.
This is the reason why some rubyist recognize that callbacks are at least problematic, if not to be avoided, while others prefer to implement node’s Express in ruby, rather than use Rails controllers.
I like the idea behind Rails. Convention over configuration is great, and I also totally subscribe to the notion that application developers should write more business-specific code, and less infrastructure code without any business value.
The problem with Rails isn’t that it’s an opinionated framework.
The problem is that its opinions are wrong:
- Tying you down to a single, err, “controversial” persistence mechanism is wrong.
- Making it impossible to do proper unit testing is wrong.
- Encouraging you to do things as side-effects rather than explicitly is wrong.
When it first launched, Rails was revolutionary: it was the first to offer such comprehensive guidelines, and support, to create your application in a standard way.
However, it seems that today, our standards of what is ‘correct’ or ‘recommended’ have changed, while Rails has stubbornly remained where it was 10 years ago.
You can still create a good application using Rails.
It’s just that it doesn’t allow you to create a great application.
17 thoughts on “Why rails sucks”
It’s been over 2 years with Ruby and Rails – I can’t stand either of them. It’s all an absolute mess and dealing with all the “magic” that Rails provides makes me work around the issues more than trying to actually solve a problem. I’m ready to just chuck everything and start over service-by-service and only doing Ruby on legacy things in maintenance mode.
Have you used Ruby without Rails? You should check out Sinatra – it has essentially zero magic and is very hands-on. Read about Rack for a few minutes and then get into Sinatra before you discounts ruby-based web frameworks altogether.
I’m just saying don’t throw the Ruby baby out with the Rails bath water, that’s not fair.
“Convention over configuration” is exactly what I hate about Rails. You hated things about Rails that also suck, but only mildly. I came to Rails by diving directly into a large codebase that was already serving millions of people.
The first thing I learned was they had these “HAML” files set up that were kind of like HTML, but with bits of Ruby mixed in, in ways that didn’t make sense (specific things I couldn’t figure out by reading the code included where is it legal to put Ruby code and where isn’t it legal?). Somehow, certain variables were defined in those HAML files and already had values… from somewhere. Where? Who knows. Even the other devs didn’t seem to fully understand the answer to that question. What are @birds? We just don’t know.
Then someone told me that the HAML files were all running within the context of a specific class. But how? Does Rails actually parse the Ruby and HAML code and stitch it together? Again, nobody seems to know. But lo and behold, instance variables and methods from a certain class can be referenced from the HAML files. How is the object the HAML is running in instantiated? What are birds? We just don’t know.
Google was useless. Since my first encounter was through HAML, and Ruby tutorials all start with the Gemfile (which I didn’t need to touch for a whole month), all documentation on HAML assumed that you already knew where the damn variables were coming from, since you did create those variables yourself, didn’t you?
It turns out that Rails connects certain things that are normally unrelated to each other and that normally don’t matter: The filenames of Ruby source files, the name of the directory each file is in (and the structure of the directories relative to each other), the names of the classes they define, the names of the methods in the classes, and the names of the HAML files are all significant to Rails. Get one wrong, and Rails won’t know where to find your code even though it’s *right fucking there*, and there’s no way to tell Rails where to look.
I eventually learned that a specific method within the class where the HAML files run gets called before each HAML file is loaded. The method has the same name as the portion of the HAML file’s name that comes before the first dot, which is also part of the URL to the HAML file (or the Ruby method). By fucking convention. Isn’t that nice?
At this point I still couldn’t figure out how to add a new URL to the server. I knew that Rails had these things called “routes” and they had something to do with URLs, but beyond that, Google was completely useless.
Again, the answer was in another unstated convention. There was a file where you put a little bit of information about each URL, and then Rails makes up the rest by assuming that you’re following the mostly-undocumented conventions. By another convention that to this day
I don’t understand, each URL I created had an ID in front of it, such as /:id/foobar. Why, and what is the significance of the ID that is expected to be provided? What are birds?
Stack overflow was also useless. Many of the accepted answers didn’t even work (possibly because Rails has changed a lot since they were written) or were completely irrelevant (Where did that ID come from? Look! There’s a bird!).
The “magic” of Ruby isn’t the “voodoo” kind of magic. It’s more like stage magic. Rails stuffs a handkerchief into its hand and then pulls out a bird (whatever that is), and it isn’t about to tell you where that bird really came from or where you should look if you need the handkerchief back.
“Convention over configuration” is the absolute worst thing about Rails. It really means “guess why your app isn’t working this time!”
LikeLiked by 1 person
Hi, I’m a Ruby developer, and I liked and used Rails a lot in the past, but nowadays (and more experienced) I totally think that Rails sucks for almost any medium to big scale project, but is still a good option if you use in the “right” way…
I’m using Grape mounted in Rails for my projects(this gives me development speed – but I can avoid all ActionController stack for each request), but my goal is to create a “mini rails” enviroment to mount Grape and drop the Rails dependency, this is a thing that can be done later, while the product get more mature, since we need to launch things faster, but also with very good quality.
(1) unit testing doesn’t need purity, it needs simplicity. Mocking everything is a pain in the ass so Rails strikes a balance by just using a database.
(2) data and business logic are inherently coupled. Sure, you can split them up, but the business logic still depends on the design of the data. All you’ve done is add another layer to little benefit.
(3) Rails has lots of methods that are there for advanced cases, if you don’t need them don’t use them.
(4) Your unit test suite shouldn’t be taking 18 minutes. I have an extensive unit test suite on all my projects and still haven’t reached 5 minutes. Perhaps try not persisting the objects when unnecessary or splitting domain logic into decorators.
I fear you have fallen into a common pitfall for people coming from Node: mistaking purity for productivity.
LikeLiked by 3 people
(1) I’ll have to disagree with you on this one.. I guess it’s a matter of opinion. I would’ve liked, however, to have a choice. Rails makes it impossible for me to do it the way I’d like to.
(3) I still think it’s a problem to be bombarded with 100s of methods, since it makes it hard for me to find the ones that I actually do need
(4) You’re right, run time can be significantly reduced by persisting only when appropriate. I guess this project was just a little naive in its implementation of tests.
I’m afraid I didn’t follow your point on (2).
Not to belabor what I said in another comment, but because it’s super relevant to (1) and (2) I want to put this here also: Rails’ folder structure is just a suggestion, and not every object has to inherit from ActiveRecord::Base. You can put whatever bare Ruby objects wherever you want to achieve an ideal object-oriented ecosystem isolated from persistence logic. Pass those objects around! 🙂
LikeLiked by 1 person
You are putting too much in your ActiveRecord models… It sounds like WAY too much – this is the root of basically all the complaints of this post. Use them for persistence only and put everything else in plain old Ruby objects and essentially all of your problems are solved. This is not Rails’s fault, it’s poor encapsulation/separation’s fault.
Especially for the tests. With proper organization you can run nearly all of your tests without involving Rails (super fast!) and include Rails for some integration tests.
Actually, reading the different comments to this post I’ve come to realize this as well.
Interesting to point out though, that we had a few experienced rails devs on our team for this project, and still ended up like this..
So I guess it’s a very easy trap to fall into.
LikeLiked by 1 person
You’re absolutely right – it’s far too common for “experienced rails devs” to abuse the framework (if you’ll excuse a sort of aggressive word). I think part of the problem there is Rails has a relatively low barrier to entry and people can just sort of coast in it for a long time, gaining the “experienced” title without ever questioning their methods or improving themselves.
If you’re interested in a super-fast and effective crash course in liberating your ruby objects from the active record monster (and speeding up your tests everywhere) along with tons of other great levelling-up tips I’d recommend the https://www.destroyallsoftware.com/screencasts/catalog screencasts. Particularly Season 3. They’re non-free, but it’s some of the best money I’ve ever spent advancing my skills.
So you went from zero to expert in a framework in 8 weeks, using one code-base as a case study? Its okay to not understand or appreciate something, labeling it as wrong….. not so much.
you’re welcome to point out anything wrong I’ve said / things I’ve missed.
Like the guy said: If I see a helicopter in a tree, I know the dude fucked up.
So what’s your recommendation?
Honestly, it depends on your what your project is.
Currently, I’m leaning towards smaller, more focused packages rather than full-blown frameworks (i.e use express / hapi in node for routing, and then pick and choose for templating, DB access, whatever else you need).
Hell, for a small scale project, I might recommend rails – it’s great for the simple stuff.
ActiveInteraction gem would help splitting concerns
I’m at work and don’t have a ton of time to look at this article in detail but you are saying that it is impossible to fake data with Rails tests?