Django and Rails: Riding 2 Horses in Midstream

Rewriting a Java app in Django and Rails

Posted by 07/24/2006

I have the luxury of rewriting a 25,000 SLOC Java application in 2 web frameworks at once simultaneously. <a href="http://rubyonrails.org">Ruby on Rails</a> and <a href="http://djangoproject.com">Django</a>. Herein I compare the two.

The Project

It's your basic project tracking, inventory type of thing. Not particulary complex, but not particulary simple either. I've managed to match the functionality of the original Java application with around 5000 lines of Ruby and Python. The Python SLOC is slightly lower at this point, but I don't consider that significant. As I've worked on this project I've been trying to decide which one I like better. They both seem inspired by the same Muse. Like the 2 versions of Calculus that spontaneously generated with no knowledge of one another. For the past 6 months I've gone back and forth between which one I like better, and which one is better for my particular application. These are a few observations from that experience:

Django (and/or Python) Annoyances

  1. I like Generic Views but when I need to venture away from them I am dwelling in a world that is too low level. The Manipulator API is clunky, like something that will substantially change in the future. If you haven't worked with it the Manipulator is an object that sits between the request and your form or response - converting data to python, validating - stuff like that. And also responsible for generating the skeleton that is the form for your domain object. Rails doesn't have such a beast - which is why a rails application does not typically create forms at runtime. Scaffolding is a code generation function - not a runtime function. This distinguishes Django with the ability to do half of scaffolding at runtime. That is quite an accomplishment and something you see in action whenever you use the admin application. It's an interesting approach, but when I work with manipulators it feels like I'm doing something I'm not supposed to be doing. Why? This is the typical code:
    def assignment_create(request,project_slug):
        manipulator = Assignment.AddManipulator()
        t = loader.get_template('projects/assignment_form.html')
        project = Project.objects.get(label__exact=project_slug)
        
        if request.POST:
            new_data = request.POST.copy()
            new_data['project'] = project.id
            
            errors = manipulator.get_validation_errors(new_data)
           
            if not errors:
                # No errors. This means we can save the data!
                manipulator.do_html2python(new_data)
                new_assignment = manipulator.save(new_data)
                return HttpResponseRedirect("/projects/view/%i/" % project_slug)
        else:
            # No POST, so we want a brand new form without any data or errors.
            errors = {}
                    
        form = forms.FormWrapper(manipulator, new_data, errors)
        
        c = RequestContext( request, {
            'form' : form,
            'project' : project,
        })
        
        return HttpResponse(t.render(c))
      
      
      
    All that, just to add an extra parameter. It involves too much of the internal workings of Django. I'd rather be able to do something more along these lines:
      
       def assignment_create(request, project_slug):
           project = Project.objects.get(label__exact=project_slug)
           form = Assignment.EntryForm()
    
           if request.POST:
               parameters['project'] = project
               assignment = Assignment.create(parameters)
           try: 
               assignment.save()
               return HttpResponse(project.url)
           except:
               # this would go back to the form with the errors or something
               form.errors = assignment.errors   
           
           c = HttpRequest( request, {
                'form' : form,
                'project' : project,
           })
        
           return HttpResponse(render('projects/assignment_form.html'))
           
      
    This is probably not good either, but you get the idea.
  2. Errors get swallowed by the templating system (see Django Good points). This is both good and bad. Some way to turn it on and off would be nice. Like the DEBUG = True. When I have my designer hat on, I like this, but when I have my programmer hat on it deceives me
  3. The admin views are great, but it would be cooler if I could actually use them in my application somehow. That does not seem workable at this point. Which is unfortunate because it's so close, and yet so far away. And it is something that could really differentiate Django from everything else
  4. The admin views are great, but they break down because they can't possibly foresee all situations. For instance, oftentimes I need filters on views of data that are filtered themselves; Example: an item has a ForeignKey to a Category whose filter choices should be limited to that particular type of object. Forms 0-100, 100-200..., Book A-F,G-L... while the category list itself contains both the 0-100,100-200... set and the A-F,G-L... set. Or I need filters that keep filtering after I enter data - so I can continue to view a subset of information. Imagine a Project object with Assignment objects. I filter the view of Assignments with a Project - add an Assignment, and I'm back looking at all the Assignments again.
  5. Migration of Data is somewhat of a pain. Hand writing Sql statements for my own data migration isn't that bad, but I've had problems with Django versions and having to update the ContenType data by hand. For a while in the admin view I could not view a User because the permissions select boxes were not populating because there was a problem with the content_type_id field in some Django specific table. This seems to be some strange dependency cycle problem because theoretically the contentype application should be seperate from the admin application, right? If not then maybe bundle them in, or have a way to specify dependencies. Or have apps install as eggs and let the setuptools take care of that kind of stuff.
  6. File upload is still unworkable for mp3s. Also, for specifying a directory into which the download will go. I looked at fixing this, but ending up just subclassing the ImageField. Maybe that's okay. In fact I even have a subclass of TextField for entering text in a input tag larger than 30. This seems particulary stupid, and yet was so easy that maybe its not that bad:
    class SubjectTextField(forms.TextField):
    
        def render(self, data):
            if data is None:
                data = ''
            maxlength = ''
            if self.maxlength:
                maxlength = 'maxlength="%s" ' % self.maxlength
            if isinstance(data, unicode):
                data = data.encode(settings.DEFAULT_CHARSET)
            return '<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
                (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and ' required' or '',
                self.field_name, str(75), forms.escape(data), maxlength)
    
    class SubjectField(Field):
       def get_manipulator_field_objs(self):
           return [SubjectTextField]
    
    
    still - I feel like I'm getting too involved with the internals. And obviously a TextField with a size=30 parameter is better. Rails just takes the normal html options when it creates an input field - so I can add style, class or any other html code. But then Rails requires more than {{ form.subject }} to make the same thing. And if I have a contact field putting {{ form.contact }} is really nice compared to
      <%= select_contact('project', 'contact') %>
      

    Helper:

    
            def select_contact(object, method)
                contacts = Contact.find(:all, :order => ['last'])
                select(object, method,
                       contacts.collect {|contact| [ contact.display_name, contact.id ] },
                       { :include_blank => true }, :class => 'input')
            end
    
      
  7. The get_absolute_url() strikes me as wrong. I have a lot of basically hard coded links all over the place - even in the model
  8. No built in Ajax. I'm not that crazy about Ajax myself - but it's one of those things that impresses people - like a flashy guitar solo
  9. Unit testing is not part of the package. This is both a blessing and a curse (see Django Good points). But I find myself missing things like built in testing and a build system especially (something like Rake)
  10. Metaprogramming in Python seems a little quirky. For instance, adding to the __metaclasses__ variable as a means of doing a Mixin is quirky. The Include mechanism of Ruby really makes sense. Also, adding properties to a class by adding to the __dict__ value seems weird whereas this code...
      controller:
    
        @user.each do |user|
          class << user
           attr_reader :totals
    
           def generate_numbers
             @totals = Assignments.count(:all, :conditions => ['user_id = ?' user.id])
           end
          end
          user.generate_numbers
        end
    
       view:
        <% @users.each do |user| %>
        <%= user.totals %>
        <% end %>
      
    although arguably a little weird too, seems a more descriptive way of actually adding runtime properties to a class. It's the same syntax for creating a class in the first place. It seems more natural but maybe it's just me; The __dict__ method is certainly quicker and more to the point. And sometimes I actually enjoy the 'bolted-on' feeling of the Python class. Because it brings me closer to the actual mechanism of a class - which is really nothing more than a dictionary of methods and properties anyway. Why make it a mysterious 'object'? Why complicate it or make it magical? Why not just add a property __dict__ to a class and allow access to it? Why not add meta-classes by adding entries in a __metaclasses__ property?

Note:Some of my objections go back to assumptions that go back to my Java days. Is it that bad to have to know some internals of code your working with? If your working with Struts and 25,000 lines of code - yes. But maybe in dealing with 4,000 lines of code and Django, it's not such an issue to dig through the code a little. Your saving so much time otherwise.

Django Good points

  1. Errors get swallowed by the templating system. Sometimes this is very good. For instance, if a Project does not have a contact I can still go {{ project.contact.first_name }} and not worry about it. In Rails I have to do some kind check like this:
      <% unless @project.contact.nil? %>
           <%= @project.contact.first_name %>
      <% end %>
      
    or I will get the "got nil when you didn't expect it" error
  2. Caching is global and can be set per settings file. This is very useful, so I can have no caching __period__ during testing. Also, I don't actually want to think about caching - so I appreciate that fact that Django just does it across the board. Rails certainly has caching, but it does nothing by default - and I don't know a good way to apply it to a particular environment. This is not to say there is not a way, but I have not seen it
  3. Even with Mongrel it __feels__ 33% faster than Rails. I have no benchmarks to support this, just my general impressions
  4. Regular Expression URL matching; more flexible than Routes. I like being able to populate forms from fragments of a URL that have been converted into request parameters. It works good for something like /users/1/add_calendar_item/ or /projects/1/add_assignment so I don't have to do the tired hidden field trick. I think I can do this with Routes, but it doesn't have the same terse, condensed effect. In Django I'm just mapping a string received in a URL to methods of a module. I don't have to create a class - and I can modularize as need be. Routes are all in the same file - and don't have the advantage of Python's named function parameters
  5. Templates can go anywhere - media can go anywhere. This is very useful in a group setting - splitting the work up between people of varying skills
  6. Debug view: This is nicely organized and useful - almost as useful as an actual IDE debugger, which is the only reason I ever use an IDE. Although Rails has the breakpoint method - Django's debug view is better because it shows snippets of the relevant code, in every method call - and all the current variables. A++. Very useful
  7. Unit testing is not part of the package. This is both a blessing and a curse (see Django Annoyances). I like the way this project just feels like I'm using another Python package. It lets me choose how to do Unit testing - it doesn't spit out a bunch of strange unexplained directories. It lets me organize my code the way the project dictates, rather than the framework. I can put all my view code in different modules. Or not.

Rails (and/or Ruby) Annoyances

  1. Always seems a pain to create an authentication system. None of the generators seem right, which is weird because the Django system works fine for me (except the get_profile() thing is very necessary). So I don't require that much
  2. I have had two times where I put an object in the session (User object) and it crashed my computer so bad I had to shut everything down. This was __really__ annoying. It might have had something to do with the fact that I was storing the session in the database. I don't know. When I switched it to save the string "username" - it worked fine
  3. Deployment is a problem - especially on Windows. Nobody cares about Windows, but particulary the people that work with Ruby on Rails. It seems like Mac, Linux, Windows is the order of preference. I hate Windows too - but it's my job - and it's 97% of the world
  4. Ruby uses too much memory. It's a resource hog. It's worse than Java. There, I've said it. And I see no signs of that improving. There is Yarv and Rite, but I find it difficult to gauge the progress of those
  5. Rails spits out directories all over the place - components, vendor, lib, script, test config, db etc... without any particular explanation. It gives a project an instant cluttered feel. For a simple project this is fine, but I find that once I start making a lot of modules and subdirectories, that some of the behaviour is unpredictable - particularly in relations and and unit tests. It's a lot better than it used to be though
  6. Naming a file projects_controller, and the resulting class ProjectsController seems weird to me
  7. It's hard to find good Ruby documentation. Google searches are not as fruitful. Maybe I'm searching wrong. Maybe I haven't figured out how to search for the language - or maybe there is a dirth of material
  8. C extension libraries are generally not distributed in binary format for Windows. In Python there is always an msi installer for most well known projects. It is easy enough to build on Linux, but Windows binaries are a nice convenience

Rails Good points

  1. It can do whatever I need it to do. It has the right balance of flexibility, usability, practicality and usefulness. That is not an easy balance to acheive. But right now I know I can use it to accomplish any web project I have. How do I know this? I can just tell
  2. Consistent design. It displays the Quality Without A Name in this way. Any small part of Rails seems like the larger whole. Django does not exhibit this quality, I'm sorry to say. I enjoy the odd, quirky feel of Django, but it does not feel like a whole being looking at me with one face. It is a lot of good ideas stitched together
  3. Being able to add a method to a class that gets called during initialization without subclassing or overriding an __init__() method is handy. For instance in the Java world I had some generic classes for a Document class. Classes like Handbook, Form etc... the package OJB was able to make a class hierarchy of these, store a generated key in a ojb_hl_seq table - and assign keys to the objects from that sequence rather than making use of auto_increment.

    In Rails I was able to continue using this key generator because I could add a has_generated_key 'document' to all the classes and viola! They all refer to the table for their key :before_save. With Django I had to make a class Document or somesuch, and then make use of multiple inheritance. Maybe this is okay. But I would have liked to have used meta-programming of some sort. However, I'm still unclear how to pull the same thing off. I'm positive it can be done, but figuring it out is another matter - and it was easy in Ruby (see below).

    
        models:
    
        module GeneratedKey
    
            def self.append_features(base)
                super
                base.extend(ClassMethods)
            end
    
            module ClassMethods
    
                def has_generated_key(options={})
                    key_configuration = { 
                        :object_name => "",
                    }
    
                    key_configuration.update(options) if options.is_a?(Hash)
    
                    class_eval <<-EOV
                      include GeneratedKey::InstanceMethods
    
                      before_create :generate_key
                      cattr_accessor :key_configuration
                      @@key_configuration = key_configuration
    
                    EOV
    
                end
            end
    
            module InstanceMethods
    
                def generate_key
                    table_name = key_configuration[:object_name]
                    key = SequenceKey.find(:first, :conditions => [ "TABLENAME = ?", table_name])
                    new_id = key.MAX_KEY + 1
                    key.MAX_KEY = new_id
                    key.update()
                    self.id = new_id
                end
    
            end
    
    
        end
    
    
        class SequenceKey < BaseRecord
            set_table_name "ojb_hl_seq"
        end
    
    
    
       class Handbook < BaseRecord
            has_generated_key :object_name => 'document'
       end
    
      
    Maybe I'm weird, but this seems fairly easy. I don't totally understand how the before_create is able to stack up multiple methods to call. I haven't figured that out yet. But it is easier than anything I could figure out in Python myself

Overall Summary

I still don't know which one I should use. So I'll just keep developing in both. It's actually a good way to come up with ideas to develop the same application in 2 languages simultaneously. I'd recommend it.

Comments

Post a comment


Total: 0.17 Python: 0.11 DB: 0.06 Queries: 31