Software Development Magazine - Project Management, Programming, Software Testing |
Scrum Expert - Articles, tools, videos, news and other resources on Agile, Scrum and Kanban |
Click here to view the complete list of archived articles
This article was originally published in the Fall 2006 issue of Methods & Tools
An Introduction to Web Development Using the Ruby on Rails Framework - Part 2
Nico Mommaerts
The Model
The model in a database-driven Web application is contained in the database. ActiveRecord lets you 'wrap' a class around each table, towhich you can add your own business logic, and voila, you have your own domainmodel. Let's see how this 'wrapping' happens when we have a table called 'employees' that we want to access. The ActiveRecord class would like this:
class Employee < ActiveRecord::Base
end
Would you believe it if I said this class lets you access all the columns in the table 'employees' and enables you to read, write, update and delete records? Do you remember the philosophy of 'Convention over Configuration'? This is a very clear example of that principle: it lets you write less code (and fewer bugs). Let's take a look at how this works under the hood.
How does Rails know we are interested in the table 'employees'?
Convention: the name of the model is the singular of the database table name.
How can you access the different columns in the table?
Convention: Rails adds attributes to the Employee class with the same name as the columns in the table. For example, the 'employees' table has a column named 'name', so the Employee class would have a 'name' attribute. The attributes are actually added at runtime, using Ruby's powerful dynamic capabilities, this is why you don't see them in the source code.
All the code required to load/save the data from/to the table is inherited from the ActiveRecord::Base class, all for free! You can add your own business methods to this class if you want.
Storing and loading Employees won't get you very far though. It would be nice if we could define a relation with some other tables. After all, we are working with a 'relational' database. Let's suppose we have an ActiveRecord class 'Company' and a database table 'companies' (remember that the classname is the singular of the tablename). In order for us to be able to ask an Employee object what company he works for, we need to tell Rails that an employee belongs to a company:
class Employee < ActiveRecord::Base
belongs_to :company
end
If 'employee' is an instance of the Employee class, we can ask it what company is attached to it:
employee = Employee.find_by_name('David Heinemeier Hansson')
# note that Rails has find_by_<column> method for each column!
puts employee.company # -> 37 Signals
Note: everything after # is considered commentary
Again, this magic works by using certain code conventions. By saying 'belongs_to :company' Rails expects to find an ActiveRecord model named Company, which in turn expects a table called Companies. The relationship between the two is figured out by the 'belongs_to' statement, which expects to find a foreign key column in the Employee table, with the name of the table it points to. The name of the foreign key in this example would be 'company_id'.
This is only one example of a possible relationship, the others are :has_one, :has_many, and :has_and_belongs_to_many. They all work using the same principle, the convention of using primary and foreign keys with a certain name. All the metadata is extracted at runtime from the database tables and there is no explicit mapping present. It is therefore not possible to generate automatically a database schema like for example Hibernate does, a well-known object-relational mapping framework in the Java world.
That doesn't mean there is not any help at all from ActiveRecord to manage your database(s). There are two classes that help you: ActiveRecord::Schema, a class that allows you to define your table and ActiveRecord::Migration, a class that helps you manage the evolution of your schema. Instead of writing SQL DDL statements to create your database, you can describe your tables or modifications in Ruby using a domain specific set of classes and methods, just like we defined relationships between various tables. Lets see how we could define our 'employees' table using a Schema class:
ActiveRecord::Schema.define do
create_table :employees do |t|
t.column :name, :string, :null => false
t.column :birthdate, :date
t.column :email, :string
end
add_index :employees, :name, :unique
end
As you can see the domain specific language used here is so clear, that no real explanation is needed: a table called 'employees' iscreated, a few columns and an index are defined. Since this is just plain Ruby,the database creation is completely database independent and supports every database that Rails supports, except for DB2.
class AddSSNToEmployees < ActiveRecord::Migration
def self.up
add_column :employees, :ssn, :string
end
def self.down
remove_column :employees, :ssn
end
end
This class has two methods: 'self.up' and 'self.down'. The first one is used when upgrading to a new version of the database, the latter for downgrading if needed. Again, you don't need to understand Ruby at all to see what is going on here, a new table called 'employees' is created and some columns are added. A 'schemainfo' table is automatically created where the current version of the database is stored. The default Rails installation puts a script in your project directory that executes all migrations in the correct order and brings your database up to date. Using Rails Migrations alleviates the pains often found when pushing changes to multiple development databases and other environments like test or production that may be multiple versions behind.
Needless to say, this is only a small portion of what you can do with ActiveRecord, but it did demonstrate the most important aspects. Conventions and a domain specific macro language let you describe your models and express relationships between them. When you can't adhere to these conventions due to external constraints (an existing database schema for example) or whatever the reason is, you can override this wherever you want to.
Methods & Tools Testmatick.com Software Testing Magazine The Scrum Expert |