Wildly underrated features of Ruby [and Rails]
I’ve spent roughly half of my career working with Ruby on Rails in some form or fashion, as well as a whole host of other tech. There are a handful of things about working with Ruby and Rails that I miss dearly when working with other languages and runtimes - particularly compiled ones.
As a disclaimer, everything below can be accompanied by the message of “with great power comes great responsibility”.
ActiveRecord transactions
ActiveRecord is wildly powerful on its own. But the way it allows you to easily and seamlessly weave together database queries with business logic is a superpower.
ActiveRecord.transaction do
results = … database query …
succeed = do_biz_logic_with_results(results)
if succeed do
… another database query …
else
… some other arbitrary code or queries …
end
end
All of the above code happens in a single database transaction. Yes, that means the transaction is held open for the duration of the code block - something you need to be aware of. But the transactional safety this brings to your entire application is gold.
There is also excellent support for nested transactions, so you generally don’t need to concern yourself with whether or not you’re already in a transaction
block. If you need transactionality, open a transaction
block wherever you are.
Dry runs for scary mutations
An extension of the above is incredibly useful for testing scary or potentially dangerous data mutations:
dry_run = true
ActiveRecord.transaction do
// Do a dangerous mutation
User.where(property: value).delete_all
// Validate the results of your mutation
puts User.count
// Rollback the transaction until you are ready to really execute
raise ActiveRecord::Rollback if dry_run
end
Rails console
Rails console is a REPL, at its core. Except that it’s a REPL with your entire Rails app loaded into context. This allows you to easily tinker and debug your application code. Where most other technologies funnel you into writing unit tests with contrived data, in Rails-land you can SSH into a running server, fire up rails c
, and run any arbitrary code you want to observe the output.
⇒ rails c
Loading development environment (Rails 6.1.4.4)
Production [1] pry(main)> User.first
User Load (3106.1ms) SELECT `users`.`id`, `users`.`email`, `users`.`email_verified`, `users`.`plan_valid_until`, `users`.`stripe_customer_id`, `users`.`status`, `users`.`created_at`, `users`.`updated_at` FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> #<User:0x00007faecda93200
id: 1,
email: "ryan@roughlywritten.com”,
email_verified: nil,
plan_valid_until: nil,
stripe_customer_id: nil,
status: "active",
created_at: Mon, 31 Jan 2022 21:37:28.000000000 UTC +00:00,
updated_at: Mon, 31 Jan 2022 21:37:28.000000000 UTC +00:00>
This makes it exceedingly easy to run one-off scripts or backfills in production.
Monkey patching and debugging production
Related to the above, Ruby allows you to “re-open” any class or method and redefine its behavior at any time. So while you’re in a console, you can swap in an entirely new method for the one that was initially loaded simply by pasting the new method in.
It’s difficult to overstate how useful this is
This is useful for testing new functionality, but paired with a debugger like pry or byebug it means you can insert breakpoints and get a live debugger in a production environment. It’s difficult to overstate how useful this is during an incident or when investigating a tricky bug.
No need for admin tooling (kinda)
The combination of the above two techniques means that you generally don’t need to spend much, or any, time building admin tooling (for engineers). At some point you’ll need tooling for Customer Support or other business functions, but in terms of engineering support and ops, you’re covered out of the box for a really long time.