In the past, to build a web application you required the skills to code in your business logic.
Rails is a Model-View-Controller web framework that uses an ORM in the form of ActiveRecord for the Model layer.
For me, like many others, Rails has a very “magical” feel to it. I decided to stop believing blindly in magic and find what happens behind the scenes and I started with ActiveRecord.
In this post I will explain what I have found.
What is ActiveRecord?
ActiveRecord is an ORM. It's a layer of Ruby code that runs between your database and your logic code.
When you need to make changes to the database, you'll write Ruby code, and then run "migrations" which makes the actual changes to the database. The cool part is that it doesn't matter what database you're using: Rails can handle pretty much all of 'em, and the method format will always be the same.
It’s possible to use ActiveRecord without Rails but in this post it's all about how they're used in RoR.
Working with ActiveRecord
Now the first thing we are going to discuss is creating a model. It’s easy to create a model inside a Rails app by using rails generate
command. If you want to create a model for a Person for example, any of the following commands will work,
rails generate model Person
rails g model Person
rails g model Person first_name last_name age:integer
The first two lines are the same thing, rails g
is just shorthand for generate
. The third one gives Rails a little more information, so it can do a little more work for you.
We're saying we want this model to have three fields: first_name
, last_name
, and age
. For first_name
and last_name
, we don't specify a type, so it defaults to a string. For age
, we specifically tell it that it should be an integer.
Among other output, you should see something like this,
create db/migrate/20130213204626_create_people.rb
create app/models/person.rb
The first file is your migration file (of course, the timestamp will be different). The second is a Ruby class that represents your Model. And here is where the magic begins.
Let’s take a look to the migration file first.
Assuming that you run the last command of the example above you would have something like this,
class CreatePeople < ActiveRecord::Migration
def change
create_table :people do |t|
t.string :first_name
t.string :last_name
t.integer :age
t.timestamps
end
end
end
Where this file come from?
Well when we run the rails generate model
command we call the model_generator.rb file in rails specifically to this two methods,
…
# creates the migration file for the model.
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference?
&& !a.has_index? } if options[:indexes] == false
migration_template "../../migration/templates/create_table_migration.rb",
File.join(db_migrate_path, "create_#{table_name}.rb")
end
def create_model_file
template "model.rb", File.join("app/models", class_path, "#{file_name}.rb")
end
…
The first method creates the migration file that we saw before and the second method creates the Model file.
Let’s take a better look to the first method, the last line in particular,
Migration_template "../../migration/templates/create_table_migration.rb", File.join(db_migrate_path, "create_#{table_name}.rb")
The …/create_table_migration.rb is a template file, so let’s take a look at it,
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
def change
create_table :<%= table_name %><%= primary_key_type %> do |t|
<% attributes.each do |attribute| -%>
<% if attribute.password_digest? -%>
t.string :password_digest<%= attribute.inject_options %>
<% elsif attribute.token? -%>
t.string :<%= attribute.name %><%= attribute.inject_options %>
<% else -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% end -%>
<% if options[:timestamps] %>
t.timestamps
<% end -%>
end
<% attributes.select(&:token?).each do |attribute| -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
<% end -%>
<% attributes_with_index.each do |attribute| -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<% end -%>
end
end
This has the same format as the migration file that we saw before. We can see here that the attributes that we pass to the Rails generate model
command are being used to generate this file.
Now that we have our Model and our migration file we have to do one more thing: we have to run rake db:migrate
. This will run our migration file and a table will be created in the database.
We are ready for the next step to use this model we just created,
p = Person.new first_name: "John", last_name: "Doe", age: 30
This will create a new Person
instance. But this information it’s not yet saved in the database. To do so we need to also run the save method.
p.save
Now the instance is saved in the database. The save
method runs an SQL query command like this one,
INSERT INTO "people" ("age", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?) [["age", 30], ["created_at", Fri, 15 Feb 2013 16:02:18 UTC +00:00], ["first_name", "John"], ["last_name", "Doe"], ["updated_at", Fri, 15 Feb 2013 16:02:18 UTC +00:00]]
Inside the abstract adapters we have this method,
# Executes an INSERT query and returns the new record's ID
#
# +id_value+ will be returned unless the value is +nil+, in
# which case the database will attempt to calculate the last inserted
# id and return that value.
#
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
sql, binds = to_sql_and_binds(arel, binds)
value = exec_insert(sql, name, binds, pk, sequence_name)
id_value || last_inserted_id(value)
end
Conclusion
Rails makes heavy use of metaprogramming techniques to provide a huge amount of functionality out of the box. This may feel like magic sometimes because we are not used to this kind of logic or because we don’t fully understand it.
One of the benefits of using Rails and ActiveRecord specifically is that ActiveRecord creates an array of attribute accessors and dynamic finder methods that match the columns in the database tables.
There are methods for all kind of transactions, insert, update, find, delete. All of them works in a similar way: the parameters are received, then a SQL query is generated, and then it is executed so the information is updated in the database.