Rails Bug: Saving Only "changed" Attributes Hurts Serialization
Really unhappy with this bug. I hope the Rails team agrees that this is a bug unlike my last “bug” report.
If you’re unfarmiliar with “changed?” attribute updating I recommend reading Living on the edge (of Rails) #14 before continuing.
Here it is, the nasty:
class Node < ActiveRecord::Base
serialize :data
end>> n = Node.create! :data => { :a => 1 }
=> #<Node id: 417950, data: {:a=>1}>
>> n.id
=> 417950
>> m = Node.find 417950
=> #<Node id: 417950, data: {:a=>1}>
>> m.data[:b] = 2
=> 2
>> m
=> #<Node id: 417950, data: {:b=>2, :a=>1}>
>> m.changed?
=> false
>> m.save!
=> true
>> m = Node.find 417950
=> #<Node id: 417950, data: {:a=>1}>Changes to the hash are never saved and that makes me really, really sad.
I’ve opened a ticket.
How I Deploy Typo Using Capistrano
I’ve been deploying all of my Rails apps with Capistrano to a MySQL, Nginx, Mongrel, God stack for a while.
I hadn’t been keeping my (this) Typo install up to date because it was such a pain to upgrade it compared to my other apps. Not anymore!
Here is my awesome Capistrano deploy task for Typo. (And my database.yml, god.config, and nginx.conf for good measure).
Any way it can be improved?
deploy.rb:set :application, 'www.johnwulff.com'
set :typo_plugins, [ 'delicious_sidebar', 'flickr_sidebar' ]
set :user, 'rails_admin'
set :deploy_to, "/var/www/#{application}"
# If you change the repository be sure to flush the server's cached checkout.
set :repository, 'http://svn.typosphere.org/typo/tags/release_5_0_3'
set :typo_plugins_url, 'http://svn.typosphere.org/typo/plugins/'
# This will substantially speed things up and save typoshphere.or's bandwidth.
set :deploy_via, :remote_cache
role :app, 'www.johnwulff.com'
role :web, 'www.johnwulff.com'
role :db, 'www.johnwulff.com', :primary => true
namespace :deploy do
task :default do
transaction do
update_code
typo_plugins.each do |plugin|
run "cd #{current_release}; script/plugin install #{typo_plugins_url}#{plugin}"
end
web.disable
symlink
# Copy local database.yml to the new remote release.
put File.read(File.join(File.dirname(__FILE__), 'database.yml')),
File.join(current_release, 'config', 'database.yml')
# Copy local nginx.conf to the new remote release.
put File.read(File.join(File.dirname(__FILE__), 'nginx.conf')),
File.join(current_release, 'config', 'nginx.conf')
# Copy local god.config to the new remote release.
put File.read(File.join(File.dirname(__FILE__), 'god.config')),
File.join(current_release, 'config', 'god.config')
migrate
end
# Stop God watches, load the fresh God config that was just uploaded, and
# then start God watches.
sudo "god stop #{application}"
sudo "god load #{File.join deploy_to, 'current', 'config', 'god.config'}"
sudo "god start #{application}"
# Reload Nginx configuration without restarting Nginx.
sudo "/etc/init.d/nginx reload"
web.enable
# Don't use sudo for cleanup. This takes a bit longer than using sudo, but
# it's not a good idea to give passwordless `sudo rm` permissions to
# anyone.
set :use_sudo, false
cleanup
set :use_sudo, true
end
endproduction:
adapter: mysql
database: johnwulff
username: johnwulff
password: *****
host: localhost#!/usr/bin/env ruby
APPLICATION = 'www.johnwulff.com'
RAILS_ROOT = "/var/www/#{APPLICATION}/current"
%w{9900 9901 9902}.each do |port|
God.watch do |w|
pid_file = File.join RAILS_ROOT, 'tmp', 'pids', "mongrel.#{port}.pid"
log_file = File.join RAILS_ROOT, 'log', "mongrel.#{port}.log"
w.uid = 'rails_admin'
w.gid = 'rails_admin'
w.group = APPLICATION
w.name = "#{APPLICATION}-mongrel-#{port}"
w.interval = 30.seconds # default
w.start = "mongrel_rails start -d -e production -a 127.0.0.1 -c #{RAILS_ROOT} --user rails_admin --group rails_admin -p #{port} -P #{pid_file} -l #{log_file}"
w.stop = "mongrel_rails stop -P #{pid_file}"
w.restart = "mongrel_rails restart -P #{pid_file}"
w.start_grace = 10.seconds
w.restart_grace = 10.seconds
w.pid_file = pid_file
w.behavior(:clean_pid_file)
w.start_if do |start|
start.condition(:process_running) do |c|
c.notify = 'john'
c.interval = 5.seconds
c.running = false
end
end
w.restart_if do |restart|
restart.condition(:memory_usage) do |c|
c.notify = 'john'
c.above = 150.megabytes
c.times = [3, 5] # 3 out of 5 intervals
end
restart.condition(:cpu_usage) do |c|
c.above = 50.percent
c.times = 5
end
end
# lifecycle
w.lifecycle do |on|
on.condition(:flapping) do |c|
c.notify = 'john'
c.to_state = [:start, :restart]
c.times = 5
c.within = 5.minute
c.transition = :unmonitored
c.retry_in = 10.minutes
c.retry_times = 5
c.retry_within = 2.hours
end
end
end
endupstream johnwulff {
server 127.0.0.1:9900;
server 127.0.0.1:9901;
server 127.0.0.1:9902;
}
server {
server_name johnwulff.com;
rewrite ^/(.*) http://www.johnwulff.com/$1 permanent;
}
server {
listen 80;
server_name www.johnwulff.com;
root /var/www/in_out_open/current/public;
access_log /var/www/www.johnwulff.com/shared/log/nginx.access.log main;
client_max_body_size 50M;
# Redirect all traffic to maintenance.html if it exists.
if (-f $document_root/system/maintenance.html){
rewrite ^(.*)$ /system/maintenance.html last;
break;
}
location / {
# redirect feed requests to feedburner, unless its the feedburner agent
if ($http_user_agent !~ FeedBurner) {
rewrite ^/articles.(rss|atom)$ http://feeds.feedburner.com/johnwulff;
rewrite ^/xml/(atom|rss|rss20)/feed.xml$ http://feeds.feedburner.com/johnwulff;
rewrite ^/rss$ http://feeds.feedburner.com/johnwulff;
rewrite ^/feed/(atom|rss|rss20).xml$ http://feeds.feedburner.com/johnwulff;
}
# Forward header information so rails can make use of it.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;
# check for index.html for directory index
# if its there on the filesystem then rewite
# the url to add /index.html to the end of it
# and then break to send it to the next config rules.
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}
# this is the meat of the rails page caching config
# it adds .html to the end of the url and then checks
# the filesystem for that file. If it exists, then we
# rewite the url to have explicit .html on the end
# and then send it on its way to the next config rule.
# if there is no file on the fs then it sets all the
# necessary headers and proxies to our upstream mongrels
if (-f $request_filename.html) {
rewrite (.*) $1.html break;
}
if (!-f $request_filename) {
proxy_pass http://johnwulff;
break;
}
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}My Beautiful Controller Spec
Today I spent a few hours refining my controller rspec pattern. I’m really pleased with it.
This controller:class AssignmentsController < ApplicationController
def index
if params[:person_id]
@person = Person.find params[:person_id]
@assignments = @person.assignments
@title = "Assignments for #{@person}"
elsif params[:course_id]
@course = Course.find params[:course_id]
@assignments = @course.assignments
@title = "Assignments for #{@course}"
else
@assignments = Assignment.find :all
@title = "All Assignments"
end
@ical_url = url_for({ :format => :ics }.merge(params))
@rss_url = url_for({ :format => :rss }.merge(params))
end
endrequire File.dirname(__FILE__) + '/../spec_helper'
describe AssignmentsController do
describe 'index action GET request with' do
describe 'no parameters' do
before(:each) do get 'index' end
it 'should successfully respond' do response.should be_success end
it 'should assign a title' do assigns[:title].should == 'All Assignments' end
it 'should assign assignments' do assigns[:assignments].should == Assignment.find(:all) end
end
describe 'a valid person_id' do
before(:each) do get 'index', :person_id => (@person = test_person) end
it 'should successfully respond' do response.should be_success end
it 'should assign a title' do assigns[:title].should == "Assignments for #{@person}" end
it 'should assign assignments' do assigns[:assignments].should == @person.assignments end
end
describe 'a valid course_id' do
before(:each) do get 'index', :course_id => (@course = test_course) end
it 'should successfully respond' do response.should be_success end
it 'should assign a title' do assigns[:title].should == "Assignments for #{@course}" end
it 'should assign assignments' do assigns[:assignments].should == @course.assignments end
end
describe 'any valid parameters' do
before(:each) do get 'index' end
it 'should assign a rss_url' do assigns[:rss_url].should == 'http://test.host/assignments.rss' end
it 'should assign an ical_url' do assigns[:ical_url].should == 'http://test.host/assignments.ics' end
end
end
endLiberal use of nesting, alignment, and inline assignments, hooray.
Beautiful specs mean I’m that much more likely to actually write them…
Can it get any better?
Visualizing Seattle 911 Real-Time Dispatch 1
Using Seattle 911 Real-Time Dispatch, Rails, Hpricot, and Google Maps I’ve built this fun visualization.

Check it out live.
Rake Task for Setting up TextMate Project 1
desc "Creates a TextMate project."
task :mate do
puts "Creating TextMate project..."
system("cd #{RAILS_ROOT} && mate INSTALL README Rakefile app config db/migrate doc lib public script test vendor")
endrake mateValidation Assertions
I like to test my validations. While I don’t really care to test the underlying code that runs the validations (I trust the Rails team to make sure they work as advertised) I do like the reassurance that I didn’t screw up. So, I’ve written this small plugin to help me test my validations.
It works simply. In one of your models you’ve got a validation like this:class User < ActiveRecord::Base
validates_presence_of :name
validates_length_of :name, :in => 4..26
validates_uniqueness_of :name
endclass UserTest < Test::Unit::TestCase
# Assert that a User's name must exist for validations to pass.
def test_validates_presence_of_name
assert_validates_presence_of User, :name
end
# Assert that a User's name must be at least 4 characters in length for
# validations to pass.
def test_validates_minimium_length_of_name
assert_validates_minimum_length_of User, :name, 4
end
# Assert that a User's name must be less than 26 characters in length for
# validations to pass.
def test_validates_maximum_length_of_name
assert_validates_maximum_length_of User, :name, 26
end
# Assert that a User's name must be unique for validations to pass.
def test_validates_uniqueness_of_name
assert_validates_uniqueness_of User, :name
end
endThat’s it. Now you can rest easy know that your validations are really, really, really in place.
To install the plugin:script/plugin install http://validation-assertions.googlecode.com/svn/trunk/validation_assertions/Associations are the Best
Person and Article models. The Article model has a foreign key: person_id. In this application the best way to get the latest Article for a Person is:
class Person < ActiveRecord::Base
has_one :latest_article,
:class_name => 'Article',
:order => 'articles.published_at DESC'
endPerson’s Articles. Something like:
has_many :articles,
:order => 'articles.published_at DESC'person.articles.firstPerson’s Articles form the database just to snag the most recent one.
You could also do something like this:
class Person < ActiveRecord::Base
def latest_article
articles.find :first,
:conditions => 'articles.published_at DESC'
end
endThat’d be a lot better than grabbing all the Articles but it still wouldn’t be great. Multiple calls to person.latest_article would execute multiple calls to the database. Slow.
Using associations is sexier than a simple finder method because you get caching for free. After the first person.latest_article call subsequent calls hit the cache. Very nice.
Now, for the impetus of this post. I was implementing just what I’ve outlined here, only slightly more complex. Instead of Articles simply belonging to People, in my application Articles belong to People through Mentions.
In this scenario, the latest_article association shown above doesn’t work because you cannot (yet) have has_one_through associations. You can do has_many_through to your hearts content, just no has_one_through.
I’m not the first to want this feature.
Much like my ticket regarding the lame behavior of validates_presence_of, it looks like this won’t be resolved soon.
Oh well, I guess I’ll have to write a dreaded finder method.
RailsConf 2007 Highlights
In no particular order:
Xen and the Art of Rails Deployment got me excited about going Xen.
Kickin’ Ass with Cache-Fu made Memcached approachable.
Standing on the Shoulders of Giants by Adam Keys was funny.
Your First Day with JRuby on Rails showed off deploying a Rails app as a WAR to GlassFish.
Custom Rails Helpers: Keeping Your Views DRY had some neat view tricks.
Jamis Buck and Michael Koziarski talked about The Rails Way.
Ze Frank, who I knew nothing about prior to the conference, turned out to not only be hilarious but brilliant.
And best of all, RailsConf 2008 will be in Portland!
RailsConf
Going to RailsConf? I am.
Here is my schedule.
Rails has_file Plugin
has_file is an easy-to-use Rails plugin that provides file manipulation methods to an ActiveRecord model.
Basic Example$ cd MyRailsApp
$ script/plugin install http://rails-has-file.googlecode.com/svn/trunkclass Node < ActiveRecord::Base
has_file :donkey
end$ script/console
Loading development environment.
>> n = Node.new
=> #<Node:0x2661e04 @attributes={"created_at"=>nil}, @new_record=true>
>> n.donkey = "secret donkey message"
=> "secret donkey message"
>> n.donkey_file_path
=> "/tmp/node-20123394-donkey"
>> n.donkey
=> "secret donkey message"
>> n.save!
=> true
>> n.donkey_file_path
=> "/Users/jwulff/Development/rails/resources/node-4-donkey"
>> n.donkey
=> "secret donkey message"
>> n.destroy
=> #<Node:0x2661e04 @attributes={"created_at"=>"2007-05-08 15:07:47"}>
>> File.exists? "/Users/jwulff/Development/rails/resources/node-4-donkey"
=> falseSome Notes:
- Temporary files are employed for unsaved records. Meaning you can use the set/get methods before saving the record.
- By default files are saved to RAILS_ROOT/resources. You can change this by passing a path in along with the declaration like so:
has_file :donkey, '/var/stuff' - Upon destruction of the recordassociated files will be deleted.
record.destroy
Older posts: 1 2