You are on page 1of 100

A Rails/Django Comparison

by Ben Askins and Alan Green

This work is licensed under the Creative


Commons Attribution-NonCommercial-
ShareAlike 2.5 License. To view a copy of this
license, visit
http://creativecommons.org/licenses/by-nc-sa/2.5/
or send a letter to Creative Commons, 543
Howard Street, 5th Floor, San Francisco,
California, 94105, USA.

The original version of this presentation is


available at http://3columns.net/habitual/docs/Pres2.odp
Rails vs. Django
We wrote the same application twice
Reader Book n m
Tag
1 1

n n

Reading
Development environments different: This was just a single trial. Really need:
● New Mac vs Old Windows laptop ● Multiple developers

● Central Coast vs Cityrail ● Multiple applications

● Multiple environments

“Multiple” means “statistically significant”

Developer skill level:


● Not identical

● Too advanced to be called ‘beginners’

● Insufficiently advanced to be ‘typical’


Didn’t consider:
● Performance

● Deployment

● Maintainability

LOC and time-to-implement ● Enterprise-friendliness

measurements are stupid:


● Not transferable to other developers

● Only rough indicator of complexity Unrepresentative example application:


● Too few pages

● No Atom/RSS feeds

● No public data entry

Development practices:
● Didn’t do it the “best way”

● Older versions of Rails and Django

● Didn’t use this or that helpful third party feature

Part-time development – start-stop effect and gaps between sessions


Some Data
Plan
$1 000 000
US$1 000 000
Time to
Implement
“You don’t understand
“This’ll be easy I’ll whip it
the power of the built-in
up in a weekend.”
admin application.”

Initial Expectations
Hours to Implement
30.00

25.00
HTML Prototype

20.00

15.00

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Develop Test data
HTML Prototype
20.00

15.00

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Project set up
Develop Test data
20.00 HTML Prototype

15.00

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Models
Project set up
20.00 Develop Test data
HTML Prototype

15.00

10.00

5.00

0.00
Rails  Django
Reader Book n m
Tag
1 1

n n

Reading
Hours to Implement
30.00

25.00
Models
Project set up
20.00 Develop Test data
HTML Prototype

15.00

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Home page
Models
20.00 Project set up
Develop Test data
HTML Prototype
15.00

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Basic pages
Home page
20.00 Models
Project set up
Develop Test data
15.00 HTML Prototype

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Admin pages
Basic pages
20.00 Home page
Models
Project set up
15.00 Develop Test data
HTML Prototype

10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Amazon interface
Admin pages
20.00 Basic pages
Home page
Models
15.00 Project set up
Develop Test data
HTML Prototype
10.00

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Data loading code
Amazon interface
20.00 Admin pages
Basic pages
Home page
15.00 Models
Project set up
Develop Test data
10.00 HTML Prototype

5.00

0.00
Rails  Django
Hours to Implement
30.00

25.00
Test, tidy
Data loading code
20.00 Amazon interface
Admin pages
Basic pages
15.00 Home page
Models
Project set up
10.00 Develop Test data
HTML Prototype

5.00

0.00
Rails  Django
Hours to Implement
30.00
26:46

25.00
Test, tidy
Data loading code
20.00 Amazon interface
Admin pages
16:36 Basic pages
15.00 Home page
Models
Project set up
10.00 Develop Test data
HTML Prototype

5.00

0.00
Rails  Django
Hours to Implement
30.00
26:46

25.00 Admin Pages


Test, tidy
Data loading code
20.00 Amazon interface
Admin pages
16:36 Basic pages
15.00 Home page
Models
Project set up
10.00 Develop Test data
HTML Prototype

5.00

0.00
Rails  Django
Hours to Implement - without Admin
30.00

25.00
Test, tidy
Data loading code
20.00 Amazon interface
18:21 Basic pages
Home page
15:39
15.00 Models
Project set up
Develop Test data
10.00 HTML Prototype

5.00

0.00
Rails  Django
Lines of Code
Lines of Code
800

700

600 Model

500

400

300

200

100

0
Rails Django
Lines of Code
800

700

600 View/Controller
Model
500

400

300

200

100

0
Rails Django
Lines of Code
800

700

600 YAML data loading


View/Controller
500 Model

400

300

200

100

0
Rails Django
# Reader test data
Ben:
id: 1
username: benj72
fullname: Ben Askins
bio: Eats books for breakfast
Alan:
id: 2
username: agreen
fullname: Alan Green
bio: Fond of snakes
Fred:
id: 3
username: fred
fullname: Fred Wilkins
bio: Loves a good romantic thriller

readers.yml
Lines of Code
800

700

600 YAML data loading


View/Controller
500 Model

400

300

200

100

0
Rails Django
Lines of Code
800

700

600 Authentication
YAML data loading
500 View/Controller
Model
400

300

200

100

0
Rails Django
Lines of Code
800

700

600 Schema Migration


Authentication
500 YAML data loading
View/Controller
400 Model

300

200

100

0
Rails Django
Lines of Code
800

700

600 HTML Helpers/


Template tags
500 Schema Migration
Authentication
400 YAML data loading
View/Controller
Model
300

200

100

0
Rails Django
Lines of Code
800

700

600 Templates
HTML Helpers/
500 Template tags
Schema Migration
400 Authentication
YAML data loading
View/Controller
300
Model

200

100

0
Rails Django

Quicker

Slightly less code

Hand-coded admin
application

Concise
Browser Browser

Web Server Web Server

Routes View urls.py Template

Controller View

Model Model
Rails Django

Database Database
Browser Browser

Web Server Web Server

Routes View urls.py Template

Controller View

Model Model
Rails Django

Database Database
ActionController::Routing::Routes.draw # Some imports here
do |map| from hrproj.hr import views

map.connect '', :controller => "home" urlpatterns = patterns('',

# restful resources (r'^$', views.index),


map.resources :books do |books|
books.resources :readings (r'^readers/$', views.reader_list),
end (r'^tags/$', views.tag_list),
(r'^books/$', views.book_list),
map.resources :readers do |readers|
readers.resources :reader_images (r'^readers/(?P<username>.*)/$',
end views.reader_detail),
(r'^tags/(?P<slug>.*)/$',
map.resources :tags views.tag_detail),
end (r'^books/(?P<slug>.*)/$',
views.book_detail),
)

/books/hitchhikers-guide-to-the-galaxy

URL Configuration
Browser Browser

Web Server Web Server

Routes View urls.py Template

Controller View

Model Model
Rails Django

Database Database
class BooksController < ApplicationController

before_filter :find_book def book_detail(request, slug):


book = get_object_or_404(Book, slug=slug)
def show queryset = ReadingOccasion.objects \
@reading_paginator, @readings = paginate .filter(book=book) \
:readings, .order_by('finished')
:conditions => ["book_id = ?", return standard_view(
@book.id] request, queryset,
end 'book_detail.html',
'readingoccasion',
private book=book)
def find_book
@book = Book.find_by_title(params[:id])
end

end

Controller / View Function


Browser Browser

Web Server Web Server

Routes View urls.py Template

Controller View

Model Model
Rails Django

Database Database
class ReadingOccasion(models.Model):
reader = models.ForeignKey(Reader)
book = models.ForeignKey(Book,
edit_inline=models.STACKED,
num_in_admin=1)
class Reading < ActiveRecord::Base finished = models.DateField(
  belongs_to :book core=True)
  belongs_to :reader reading_time = models.FloatField(
end max_digits=5,
decimal_places=2,
core=True,
blank=True)
notes = models.TextField(
maxlength=2000,
blank=True)
class ReadingOccasion(models.Model):
reader = models.ForeignKey(Reader)
book = models.ForeignKey(Book,
edit_inline=models.STACKED,
num_in_admin=1)
class Reading < ActiveRecord::Base finished = models.DateField(
  belongs_to :book core=True)
  belongs_to :reader reading_time = models.FloatField(
end max_digits=5,
decimal_places=2,
core=True,
blank=True)
notes = models.TextField(
maxlength=2000,
blank=True)
Schema evolution
class CreateReadings < ActiveRecord::Migration
def self.up
create_table :readings do |t|
t.column "book_id", :integer
t.column "reader_id", :integer
t.column "date_read", :datetime
t.column "reading_time", :integer
t.column "notes", :text
end
end

def self.down
drop_table :readings
end
end

Schema is Versioned
● Drop database tables
● Re-create tables
manage.py syncdb
python yaml/load_data.py

● In production, you write migration DDL by hand

Django DB evolution
Browser Browser

Web Server Web Server

Routes View urls.py Template

Controller View

Model Model
Rails Django

Database Database
books/show.rhtml:
<%= render :partial => 'readings/list',
:locals => {:key_field => "Reader"} %>

readings/_list.rhtml: book_detail.html:
<tbody> <tbody>
<%= render :partial => 'readings/reading', {% for ro in readingoccasion_list %}
:collection => @readings, <tr>
:locals => {:key_field => key_field} %> <td class="name_col">
</tbody> <a href="{{ ro.reader.get_absolute_url }}">
{{ ro.reader.name }}
readings/_reading.rhtml: </a>
<tr> </td>
<td class="name_col"> <td class="date_col">
<% if key_field == "Book" %> {{ ro.finished|date:"j M Y" }}
<%= link_to reading.book.title, </td>
book_url(reading.book) %> <td class="num_col">
<% else %> {{ ro.reading_time }}
<%= link_to reading.reader.fullname, </td>
reader_url(reading.reader) %> <td>{% firstof ro.notes "-" %}</td>
<% end %> </tr>
</td> {% endfor %}
<td class="date_col"> </tbody>
<%= reading.date_read_for_display %>
</td>
<td class="num_col">
<%= reading.reading_time %>
</td>
<td><%= reading.notes %></td>
</tr>

View / Template
readers/show.rhtml:
<%= render :partial => 'readings/list',
:locals => { :key_field => "Book" } %>

reader_detail.html:
<tbody>
{% for ro in readingoccasion_list %}
<tr>
<td class="name_col">
<a href="{{ ro.book.get_absolute_url }}">
{{ ro.book.title }}
</a>
</td>
<td class="date_col">
{{ ro.finished|date:"j M Y" }}
</td>
<td class="num_col">
{{ ro.reading_time }}
</td>
<td>{% firstof ro.notes "-" %}</td>
</tr>
{% endfor %}
</tbody>

View / Template
And the other bits
● Can save a lot of time
● Good looking result
● Does simple CRUD quite well
but...
● Only does simple CRUD
● Only does simple relationships
● Security not fine-grained
● Not intended for public-facing pages

Django Admin application


page.visual_effect :fade, dom_id(@tag)
page.replace_html “feedback”, “Tag Deleted”
page.visual_effect :appear, “feedback”, :queue => :end
page.visual_effect :fade, “feedback”, :queue => :end

AJAX
Books on Amazon

11
10
9
8
7
6
5
4
3
2
1
0
Rails
Django
Jobs on seek.com.au
120
110
100
90
80
70
60
50
40
30
20
10
0
Ruby Ruby on Rails Python Django
● Began Oct 2003 ● Began Fall 2003
● DHH ● Adrian and Simon
– in reaction to PHP – “ditched” PHP
● Extracted from ● Extracted from
Basecamp ljworld.com
● Released: July 2004 ● Released: July 2005
● 1.0 shipped Dec 2005 ● 1.0 not yet shipped
– Latest is 1.1.6 – Latest is 0.95

History
Conclusion
Already using Rails?
Already using Rails?
Already using Django?
Already using Django?
Already know Ruby?
Already know Ruby?
Already know Python?
Already know Python?
Private admin pages?
Private admin pages?
Simple
AJAX?
Simple
AJAX?
Non-programming
web designers?
Non-programming
web designers?
Evolving Schema?
Evolving Schema?
Maturity
Maturity – product,
community, and market
Maturity – product,
community, and market
Concise or Explict?
Concise Explicit
Still can’t choose?
Thanks!
● Photos
– Sad puppy:
http://www.flickr.com/photos/sookie/108356632/
● Software
– David A. Wheeler’s Sloccount
● http://www.dwheeler.com/sloccount/
– HTML Template by Andreas Viklund
● All of the paper reviewers
● Our bosses:
– Cirrus Technologies
– Karen Askins

With thanks to
● “The Builders of Basecamp”
– http://www.oreillynet.com/pub/a/network/2005/03/10/base
● Snakes and Rubies presentation
– http://video.google.com/videoplay?docid=2939556954580
● Django FAQ
– http://www.djangoproject.com/documentation/faq/
Lots of interest in these two frameworks

Similar in some ways

Different in others

How to choose between them?

Why Comparing?
This is some Ruby code This is some Python code
Bonus Material
def standard_view(request, queryset, template_name,
template_object_name, **extra_context):
""" Wrapper around the object_list generic view.
"""
return object_list(request,
queryset=queryset,
allow_empty=True,
template_name=template_name,
template_object_name=template_object_name,
page=get_page(request),
paginate_by=PAGE_SIZE,
extra_context=extra_context)

def get_page(request):
""" Determines the current page number.
"""
return int(request.GET.get('page', 1))

standard_view
Hours to Implement
30.00
20:40

25.00
Test, tidy
Data loading code
20.00 Amazon interface
Admin pages
10:30 Basic pages
15.00 Home page
Models
Project set up
10.00 Develop Test data
HTML Prototype

5.00

0.00
Rails  Django