About four months ago I started writing a couple of projects in Ruby on Rails, and the more I used it, the more amazed I was at how easy it was and how fast I was advancing.
The purpose of this post is to share some of the things I love about Ruby on Rails.
Whenever I worked on a web project one of the things that always annoyed me was that the directory structure never felt "right", I kept moving files around, trying to make it better, but it was never perfect.
Ruby on Rails apps have the best directory structure I have ever seen (part of the reason it's so good is because of the asset pipeline which I'll talk about later):
When I need to add a new component I never have to think twice about where to put it, everything has its place.
To create a new rails project (with the entire directory structure) just run
rails new {app-name}
.
ActiveRecord is Ruby on Rails' default database layer, its best feature in my opinion is migrations.
A migration is basically a database patch, for example, a migration that adds a new table looks like this:
class CreateBooks < ActiveRecord::Migration
def change
create_table :books do |t|
t.string :title
t.string :summary
t.string :author
t.float :price
t.timestamps
end
end
end
The t.timestamps
automatically adds the "last updated" and "date created"
fields.
Another example of a migration is changing the type of a field:
class ConvertPriceToInteger < ActiveRecord::Migration
def up
change_column :books, :price, :integer
end
def down
change_column :books, :price, :float
end
end
You can write complex migrations use active record models to convert data (instead of writing lots of SQL code).
You can take any version of the database and run the new migrations on it by
just running rake db:migrate
.
As your application grows and changes, your database needs to adapt as well, and this is the best solution I've found for this problem (at least for non-NoSQL databases).
You can define model validations like this:
class Book < ActiveRecord::Base
validates_presence_of :title, :author, :price
validates :price, numericality: { greater_than: 0 }
end
And when you try to save the object:
mybook = Book.new
mybook.save() # returns false
it will return false and you can access the validation errors via
mybook.errors
.
You can also add custom validations like:
class Book < ActiveRecord::Base
validate :my_custom_validation
def my_custom_validation
unless /^a/.match(title).nil?
errors.add :title, \
'books starting with "a" are not allowed!'
end
end
end
The best thing about this is that it connects directly with the views generated
by the rails g scaffold
command so you just define the validations in one
place and the errors will be displayed in the browser.
Active Record also allows a simple way to define relationships between models, for example:
class LineItem < ActiveRecord::Base
belongs_to :order
end
class Order < ActiveRecord::Base
has_many :lineitem
end
This allows you do do stuff like lineItem.order.name
and
order.lineitems.length
(by default this links aren't loaded, they will be
loaded when you try to access the lineitems
or order
properties or if you
use the include
option when querying the model).
It is now becoming a standard to use higher level languages such as SASS and Coffeescript instead of plain CSS and Javascript.
Both languages allow writing much cleaner and readable code, and SASS adds a lot of useful features to CSS like mixins, inheritance, variables and more.
The only problem with using these languages is that they must be compiled into css and javascript for the browser to understand them. Yhat's where the asset pipeline comes in, it will automatically find all assets (stylesheets, javascripts, coffeescripts) in the app/assets and vendor/ directories and serve them to the client.
A very useful feature of this is that in development mode all of the assets are served independently (so it's easier to debug), but when switching to production mode all of the assets are combined into a single compressed file (one .js file and one .css file, but you can customize it to create packages).
The asset pipeline also takes care of caching the resources and cache-busting when uploading a new version.
So when you want to add a new sass/coffeescript file just create in the app/assets/ directory and rails will take care of the rest.
Rails has the best localization implementation I have ever worked with, all you need is a simple config/locales/{lang}.yml file which looks something like this:
he:
errors:
messages:
blank: אנא הכנס ערך
equal_to: חייב להיות שווה ל-%{count}
number:
separator: .
unit: $
human:
decimal_units:
units:
billion: ביליון
thousand: אלף
storage_units:
units:
byte:
one: בייט
other: בתים
gb: ג'יגה-בייט
date:
formats:
default: ! '%d.%m.%y'
long: ! '%e ב%B, %Y'
short: ! '%e %b'
activerecord:
models:
attributes:
book:
title: שם
summary: תקציר
author: סופר
price: מחיר
This will automatically be loaded by Rails when once you change the locale to
"he", and will affect date/time formatting, views that use form helpers such as
f.label_for
.
If you just want to get the localized version of on of these items you can do:
I18n.t('activerecord.models.attributes.book.title')
And it will translate it for you.
It also supports pluralization like this:
I18n.t('human.storage_units.byte', count: 4)
Which will return "4 בתים".
This is just brilliant, think how many times you wrote code like this:
if (count == 1)
... write the singular version
else
... write the plural version
you don't need that anymore!
Ruby is the most test friendly platform I have ever used, everything can be stubbed and mocked.
When you use rails generators to create scaffolds, controllers and all the other stuff it can generate it always generates the base tests for them, so when you want to add tests all you have to do is open the relevant file and start writing.
The testing framework I like the most with ruby is RSpec, the hierarchal format of the tests makes them really easy to both write and read, for example:
describe Book do
describe "#save" do
context "with no values" do
before :each do
@book = Book.new()
@result = @book.save()
end
it "returns false" do
@result.should be_false
end
it "has an error for the 'title' field" do
@result.errors[:title].type.should == 'required'
end
end
end
end
When running rspec the output will look like this:
Done
#save
with no values
returns false
has an error for the 'title' field
Very readable! very comfortable to write.
There's another testing framework for Rails called "Cucumber", it's designed for acceptance tests mostly (from what I understand) and it looks like this:
Feature: name of the feature
As a user
I want this feature
So I will have this feature
Scenario: doing something
Given a form
When I click the button
Then something happens
I use this with capybara and selenium to automate acceptance tests, and what I love about this is this:
An annoying issue about running tests in Rails is that it takes a few seconds to load the rails framework. Fortunately, there's a tool called "Spork" that fixes this problem by running a server with a preloaded version of the rails server and forks a copy of it every time you run your tests. I've been using it for the past months and it has improved my tests from ~4 seconds to ~50 milliseconds, so I highly recommend it.
Database testing!!
When running your tests Rails connects to a testing database (can be a local sqlite db) so you can run all of sorts of database-dependant tests (unique values, etc...).
As of the time of writing this there are 35,950 gems in rubygems. I have only scraped the surface and these are some of the really cool gems I've found:
I just love this language! these examples are freakin' amazing:
if date < 1.year.ago
...
end
3.times do
...
end
And I love the fact that you don't need brackets to call a method.
With tons of rails-related Vim plugins such as vim-rails, vim-ruby, vim-haml, vim-coffeescript and many more I have been really enjoying working on Ruby on Rails projects with Vim.
I have been using Vim for about 15 years now, and I'm amazed at how I keep learning new things about it and improving my vim-skills.
Hope you enjoyed this post, I know I did :-) Thanks for reading,
David.