A Rails/Django Comparison by Ben Askins and Alan Green

This work is licensed under the Creative Commons Attribution-NonCommercialShareAlike 2.5 License. To view a copy of this license, visit 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
http://creativecommons.org/licenses/by-nc-sa/2.5/

Rails vs. Django

We wrote the same application twice

Reader
1 n n 1

Book

n

m

Tag

Reading

Development environments different: ● New Mac vs Old Windows laptop ● Central Coast vs Cityrail

This was just a single trial. Really need: ● Multiple developers ● Multiple applications ● Multiple environments “Multiple” means “statistically significant”

Developer skill level: ● Not identical ● Too advanced to be called ‘beginners’ ● Insufficiently advanced to be ‘typical’

LOC and time-to-implement measurements are stupid: ● Not transferable to other developers ● Only rough indicator of complexity

Didn’t consider: ● Performance ● Deployment ● Maintainability ● Enterprise-friendliness 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

“This’ll be easy I’ll whip it up in a weekend.”

“You don’t understand the power of the built-in 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 HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Reader
1 n n 1

Book

n

m

Tag

Reading

Hours to Implement
30.00 25.00
Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Home page Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Admin pages Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Amazon interface Admin pages Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Data loading code Amazon interface Admin pages Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00 25.00
Test, tidy Data loading code Amazon interface Admin pages Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00
26:46

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

20.00
16:36

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement
30.00
26:46

25.00

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

20.00

15.00

10.00

5.00

0.00

Rails 

Django

Hours to Implement - without Admin
30.00 25.00
Test, tidy Data loading code Amazon interface Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00

18:21 15:39

15.00

10.00

5.00

0.00

Rails 

Django

Lines of Code

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
Model

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
View/Controller Model

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
YAML data loading View/Controller Model

# 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 500 400 300 200 100 0 Rails Django
YAML data loading View/Controller Model

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
Authentication YAML data loading View/Controller Model

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
Schema Migration Authentication YAML data loading View/Controller Model

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
HTML Helpers/ Template tags Schema Migration Authentication YAML data loading View/Controller Model

Lines of Code
800 700 600 500 400 300 200 100 0 Rails Django
Templates HTML Helpers/ Template tags Schema Migration Authentication YAML data loading View/Controller Model

● ●

Quicker Slightly less code

Hand-coded admin application Concise

Browser

Browser

Web Server

Web Server

Routes

View

urls.py

Template

Controller

View

Model Rails

Model Django

Database

Database

Browser

Browser

Web Server

Web Server

Routes

View

urls.py

Template

Controller

View

Model Rails

Model Django

Database

Database

ActionController::Routing::Routes.draw do |map| map.connect '', :controller => "home" # restful resources map.resources :books do |books| books.resources :readings end map.resources :readers do |readers| readers.resources :reader_images end map.resources :tags end

# Some imports here from hrproj.hr import views urlpatterns = patterns('', (r'^$', views.index), (r'^readers/$', views.reader_list), (r'^tags/$', views.tag_list), (r'^books/$', views.book_list), (r'^readers/(?P<username>.*)/$', views.reader_detail), (r'^tags/(?P<slug>.*)/$', views.tag_detail), (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 Rails

Model Django

Database

Database

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

Controller / View Function

Browser

Browser

Web Server

Web Server

Routes

View

urls.py

Template

Controller

View

Model Rails

Model Django

Database

Database

class Reading < ActiveRecord::Base   belongs_to :book   belongs_to :reader end

class ReadingOccasion(models.Model): reader = models.ForeignKey(Reader) book = models.ForeignKey(Book, edit_inline=models.STACKED, num_in_admin=1) finished = models.DateField( core=True) reading_time = models.FloatField( max_digits=5, decimal_places=2, core=True, blank=True) notes = models.TextField( maxlength=2000, blank=True)

class Reading < ActiveRecord::Base   belongs_to :book   belongs_to :reader end

class ReadingOccasion(models.Model): reader = models.ForeignKey(Reader) book = models.ForeignKey(Book, edit_inline=models.STACKED, num_in_admin=1) finished = models.DateField( core=True) reading_time = models.FloatField( 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 Rails

Model Django

Database

Database

books/show.rhtml: <%= render :partial => 'readings/list', :locals => {:key_field => "Reader"} %> readings/_list.rhtml: <tbody> <%= render :partial => 'readings/reading', :collection => @readings, :locals => {:key_field => key_field} %> </tbody> readings/_reading.rhtml: <tr> <td class="name_col"> <% if key_field == "Book" %> <%= link_to reading.book.title, book_url(reading.book) %> <% else %> <%= link_to reading.reader.fullname, reader_url(reading.reader) %> <% end %> </td> <td class="date_col"> <%= reading.date_read_for_display %> </td> <td class="num_col"> <%= reading.reading_time %> </td> <td><%= reading.notes %></td> </tr>

book_detail.html: <tbody> {% for ro in readingoccasion_list %} <tr> <td class="name_col"> <a href="{{ ro.reader.get_absolute_url }}"> {{ ro.reader.name }} </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

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 Only does simple CRUD Only does simple relationships Security not fine-grained Not intended for public-facing pages

but...
● ● ● ●

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 DHH

● ●

Began Fall 2003 Adrian and Simon

in reaction to PHP

“ditched” PHP

Extracted from Basecamp Released: July 2004 1.0 shipped Dec 2005

Extracted from ljworld.com Released: July 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/ David A. Wheeler’s Sloccount

Software
– –

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 http://www.djangoproject.com/documentation/faq/

Django 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 Amazon interface Admin pages Basic pages Home page Models Project set up Develop Test data HTML Prototype

20.00
10:30

15.00

10.00

5.00

0.00

Rails 

Django