ActiveRecord has a feature called composed_of that allows you to take multiple fields of a model and treat them as one object. Let's say you have a requirement for your application to track the phone number of each user. Also, you need to be able to query for users by parts of the phone number like area code, prefix, line number and extenstion. So the phone number (212) 123-4567 x55 needs to be stored in separate fields as :area_code => 212, :prefix => 123, :line_number => 4567 and :extenstion => 55. So first we create a rails app with a user record with those fields:
$ rails myapp
$ cd myapp
$ script/generate model user name:string \
phone_number_area_code:integer \
phone_number_prefix:integer \
phone_number_line_number:integer \
phone_number_extension:integer
$ rake db:migrate
What we would like to do is treat all of those phone_number_* columns as one object. So first let's create a base object that has some of the plumbing functionality. Put this in app/models/value_object.rb:
class ValueObject
class << self
def has_fields(*args)
(class << self; self; end).send(:define_method, :fields) do
args.map(&:to_s)
end
fields.each{|f| attr_reader f}
end
def mapping
fields.map{|e| ["#{name.underscore}_#{e}", e]}
end
def mapper
lambda do |params|
PhoneNumber.new *PhoneNumber.fields.map{|e| params[e].to_i}
end
end
end
def initialize(*args)
args.each_with_index do |a, i|
instance_variable_set "@#{self.class.fields[i]}", args[i]
end
end
def to_s
"(#{area_code}) #{prefix}-#{line_number} x#{extension}"
end
def ==(o)
o && self.class.fields.all?{|f| self.send(f) == o.send(f)}
end
end
And here's our subclass of that specific for the phone number. Add the PhoneNumber class at app/models/phone_number.rb:
class PhoneNumber < ValueObject
has_fields :area_code, :prefix, :line_number, :extension
def to_s
"(#{area_code}) #{prefix}-#{line_number} x#{extension}"
end
end
Rather than explain what this does, I'll just give you the specs:
require File.dirname(__FILE__) + '/../spec_helper'
describe PhoneNumber do
before(:each) do
@phone_number = PhoneNumber.new(212, 123, 4567, 55)
end
it "should have an area_code getter" do
@phone_number.area_code.should == 212
end
it "should have an area_code getter" do
@phone_number.prefix.should == 123
end
it "should have an area_code getter" do
@phone_number.line_number.should == 4567
end
it "should have an area_code getter" do
@phone_number.extension.should == 55
end
describe "#==" do
it "should be equal if both number match" do
@phone_number.==(PhoneNumber.new(212, 123, 4567, 55)).should be_true
end
it "should not be equal if both numbers do not match" do
@phone_number.==(PhoneNumber.new(212, 123, 4567, 54)).should be_false
end
end
describe "#to_s" do
it "should format the phone number as (212) 123-4567 x55" do
@phone_number.to_s.should == "(212) 123-4567 x55"
end
end
end
After you've had a chance to read through that, it should be pretty clear what the PhoneNumber class does. Next we modify the User model to use this class:
class User < ActiveRecord::Base
composed_of :phone_number, :mapping => PhoneNumber.mapping, &PhoneNumber.mapper
end
The ValueObject base class provides us with the methods mapping and mapper, which are needed to get our User model's phone_number method to do what we want. Here's what we want it to do:
it "should be able to set phone number from hash" do
@user.phone_number = {"area_code" => "212", "prefix" => "123",
"line_number" => "4567", "extension" => "55"}
@user.phone_number.should == PhoneNumber.new(212, 123, 4567, 55)
end
it "should be able to find a user by area code" do
@user.phone_number = PhoneNumber.new(212, 123, 4567, 55)
@user.save
@user.should == User.find_by_phone_number_area_code(212)
end
Now we can treat our phone number as a single object, but collect and store the values as separate fields.
Some Rails applications accept images uploaded by users via the application, such as profile images. You have a few choices as to how to store the image. I'm not going to go over them or debate the merits of each in this article. Instead I'm going to cover how to handle it if you've decided that you want to store your images in the database.
So to get started with the example, create a rails app and an image model to hold the image data:
$ rails myapp
$ cd myapp
$ script/generate model image file_name:string content_type:string \
file_size:integer file_data:binary
$ rake db:migrate
Ok, now let's put the awesomest image ever into our database:
Next make sure caching is turned on for your development environment. Edit this line in config/environments/development.rb:
config.action_controller.perform_caching = true
And finally, start up rails and view the image in your browser at http://localhost:3000/images/awesome.jpg. If you look in your log, you will see a line like this:
Cached page: /images/awesome.jpg (0.00100)
But if you refresh your browser or hit the same URL from a different browser, you will not see that anymore. That's because we've cached the file to local disk and it can be served directly from there by the web server without going through rails. Mongrel does this by default on your local development setup, but your production environment might require some configuration to make sure that happens. The guys at Rails envy have a great article that has the details on how to configure our web and application servers to do that. The article provides more detail on page caching as well, including how to clear the cache when images change, so you should definitely check that out.
Often when working with Rails applications, you need to import data from other sources. A common source is an excel spreadsheet. A simple import consists of reading each line in the spreadsheet and creating a record in the database for each line. You could do this is a small Ruby script with SQL, you wouldn't need Rails. But sometimes the import is more complicated. For example, you may want to run your application validation logic on each record. Also, maybe you need to create associated record for each row.
To handle this kind of thing, it can be helpful to use your ActiveRecord data model. To do that, you can simply create a Ruby script and add these few lines at the top:
This will boot up the Rails environment when your script starts, and then you have full access to your Rails models. You could write a procedural script to handle that, but I've found that creating an object-oriented class gives you a little bit cleaner, more re-usable framework. So let's just get right to the code. Here is the code for a base class for your data import:
class DataImport
attr_reader :file, :fields, :row_map, :default_e
#Create DSL methods for subclasses
class << self
def default_environment(env)
self.send(:define_method, :default_environment) do
env
end
end
def default_file(file_name)
self.send(:define_method, :default_file) do
file_name
end
end
end
def initialize(env, file)
load_rails(env || respond_to?(:default_environment) ?
default_environment : "development")
@file = file || default_file
raise "You must specify a file" unless @file
end
def self.run(env, file)
new(file, env).run
end
def run
open(file).each_with_index do |line, i|
initialize_row!(line, i)
end
end
def initialize_row!(line, i)
tokenize_row!(line)
if i < 1
initialize_fields!
else
initialize_row_map!
process_row
end
end
def process_row
puts row_map.inspect
end
private
def tokenize_row!(line)
@row = line.split('|')
end
def initialize_fields!
@fields = @row.map{|e| e.chomp.to_sym}
end
def initialize_row_map!
@row_map = {}
@row.each_with_index do |c, i|
@row_map[fields[i]] = c.blank? ? nil : c.strip
end
end
def load_rails(env)
ENV['RAILS_ENV'] = env
require File.join(File.dirname(__FILE__), "..", "..", "config", "boot")
require File.join(File.dirname(__FILE__), "..", "..", "config", "environment")
end
end
Alright, that's a pretty big chunk of code, but this is the implementation of a base class that you will reuse. Don't worry, your actual import class will be much shorter. In other words, you can copy and paste this right into your app and use it as is, but if you are interested to find out how it works, read the next few paragraphs.
So the first interesting thing you'll encounter in this code is the DSL-ish methods. To understand how this works, you really need to read Why The Lucky Stiff's Seeing Metaclasses Clearly. The talk Dave Thomas gave just the other day at the NovaRUG would help too. But basically what it does is define 2 class methods that are intended to be used by subclasses during class definition. When called, they will define methods that the base class can then use. This the concept I blogged about the other day in action. They are conceptually the same thing as the definition of the belongs_to and has_many methods in ActiveRecord. It will make more sense when you see an implementation.
Next up is the constructor which handles setting the file instance variable for our data import class, as well as loading up rails with the right environment specified. After that are class and instance methods both called run. The idea here is that we want to work with an instance of the data import class, but it will be convenient to just call OurDataImport.run.
The work happens in the run instance method. This opens up the file and starts processing it line by line. In this method I'm trying to employ a technique, or more of a style I guess, that Marcel Molina spoke about at the DC Ruby Users Group. The idea is that you should strive to as much as possible have all of the code within a method be at the same level of abstraction. If you look at this whole method:
def initialize_row!(line, i)
tokenize_row!(line)
if i < 1
initialize_fields!
else
initialize_row_map!
process_row
end
end
It's easy to read it and understand what it is going to do. First we are going to tokenize the row, then if it is the first row, we will initialize the fields, otherwise, we will initialize the row map and process the row. For example, this method could be written like this:
def initialize_row!(line, i)
tokenize_row!(line)
if i < 1
initialize_fields!
else
@row_map = {}
@row.each_with_index do |c, i|
@row_map[fields[i]] = c.blank? ? nil : c.strip
end
process_row
end
end
But there is an abstraction-level switching that you have to go through mentally once you get to the first line after the else. The rest of the method is composed of intent-revealing methods, but then we just have this lower-level chunk of code that deals with setting instance variables. So don't do that, the other implementation is cleaner, leads to code that is composed well and is easier to test and extend.
So the meat of what happens here is that the run method reads in the file row by row. It assumes the data will be pipe-separated (that is, records separated with the "|" character), because I find that to be easiest to parse. It's trival to convert an excel spreadsheet to a pipe-separated text file using OpenOffice. If your data is not pipe-separated, you could override tokenize_row to split up the row some other way. It assumes the first row contains the field names that each column will map to, so if we are on the first row, it just stores away the field names. Then, on each subsequent row it constructs a map (a.k.a hash) containing the column name and values. Then it calls the process_row method. The implementation of the process_row doesn't do anything interesting in this base class because the intent is for you to override that in your subclass.
Ok, so now let's put this to use. Create a rails app with a simple user model:
$ rails myapp
$ cd myapp
$ script/generate model user name:string email:string
$ rake db:migrate
Now copy the whole base DataImport class from above into db/data/data_import.rb. Then create a data file at db/data/users.txt with something like this:
name|email
Paul Barry|mail@paulbarry.com
Someone Else|someone_else@example.com
And then finally we'll create an implementation of our data import at db/data/user_data_import.rb
require 'data_import'
class UserDataImport < DataImport
default_file "users.txt"
def process_row
user = User.create!(row_map)
puts "Created => #{user.inspect}"
end
end
UserDataImport.run(ARGV[0], ARGV[1])
So now we have a pretty clear, concise file that explains what we are doing. You can see the call to default_file that allows us to set our default file name using a clean, DSL-ish syntax. We could also call default_environment there as well if we wanted to, but we don't have to. This is a very simple import where we just create a user for each row. The last line of the script runs the import, passing in the command line arguments. If you pass no arguments, it will work, using "development" for the environment and "users.txt" for the file name. A real data import is likely to do some more interesting work with the data, but at least this gets all the plumbing of processing the data file out of the way for you and allows you to focus on the logic of what you need to do with the data. All that's left to do is simply run the db/data/user_data_import.rb script.
Sidenote: I've found that if your want to run the script from textmate, you need to add this line top of your script, due to a conflict in the ruby libraries provides with TextMate.
Many applications send emails out to users when certain actions happen, such as placing an order. Sometimes the contents of those emails can change frequently, so it is a nice feature is to be able to give the administrative users of your system a way to change the contents of those emails.
One way to do it is to use the Liquid templating language. The reason Liquid is a good choice is that Liquid is "non-evaling", which means you can just execute arbitrary Ruby code from within a Liquid template. This isn't true of ERB, for example, where you could just put <% something_evil %> in the middle of a template and it would get executed. So if you were to give your users a way to edit the the template and it was using ERB, you would have a potential security problem.
Now you can create a new mail template through the browser or just using IRB:
$ script/console
>> MailTemplate.create!(:name => "order_confirmation",
:body => "Dear {{name}},\n\nThank you for your order!")
There you can see the very basic syntax of a Liquid template. The {{name}} is a variable that will get replace with content when we render the template. Now we'll generate a mailer:
Now we have to modify the OrderMailer to use our Liquid template stored in the database. Edit app/models/order_mailer.rb so that it has the following contents:
class OrderMailer < ActionMailer::Base
def order_confirmation(sent_at = Time.now)
@from = 'orders@yoursite.com'
@recipients = 'some_customer@whatever.com'
@subject = 'Order Confirmation'
@body = {"name" => "Paul"}
@sent_on = sent_at
@headers = {}
end
def render_message(method_name, body)
mail_template = MailTemplate.find_by_name(method_name)
template = Liquid::Template.parse(mail_template.body)
template.render body
end
end
The order_confirmation method should look familiar, it is the almost the same as it would be for any regular mailer. One caveat is that Liquid expects the keys in the context hash that you pass to it to be strings, not symbols. The @body instance variable is the context hash that will be passed to Liquid. Those are the variables that you will have access to when the template renders.
The real work is happening in the render_message method. This is the method that the mailer calls when delivering your message. Normally you don't need to override this method, it takes care of finding the ERB template and rendering it for you. We can override it here to look for our Liquid template from the database. The first argument to the method will be set to the method name of the mailer, which in this case is order_confirmation. The second argument will be the value that we set @body to in the order_confirmation method. This makes for a pretty clean way of doing this, hooking in our Liquid functionality using the standard object-oriented technique of overriding a method, no monkey patching required, which should make Avdi proud!
Make sure you've started the app with script/server and then go into script/console and enter this:
>> OrderMailer.deliver_order_confirmation
Now in the output from script/server, you should see something like this:
Sent mail:
Date: Sat, 19 Apr 2008 06:41:53 -0400
From: orders@yoursite.com
To: some_customer@whatever.com
Subject: Order Confirmation
Mime-Version: 1.0
Content-Type: text/plain; charset=utf-8
Dear Paul,
Thank you for your order!
Editorial: 4 years ago, Matt Raible posted an article to the The Server Side titled Sending Velocity-based Email with Spring. Consider this the Ruby equivalent to that article. Compare the amount of code/configuration required to accompish this task in Java/J2EE vs. Ruby on Rails. Also, the Ruby version is more complicated because it involves storing/retrieving the templates from the database rather than the file system. If we were just retrieving the templates from the file system, this probably would have been a 2 line article. This is just another example of how much simpler Rails makes things when compared to Java/J2EE.