Django and Rails: Riding 2 Horses in Midstream
Rewriting a Java app in Django and Rails
Posted by 07/24/2006
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
- 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. - 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 - 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
- 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
ForeignKeyto aCategorywhose 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 aProjectobject withAssignmentobjects. I filter the view of Assignments with a Project - add an Assignment, and I'm back looking at all the Assignments again. - 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
ContenTypedata by hand. For a while in the admin view I could not view aUserbecause 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. - 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 ofTextFieldfor entering text in ainputtag 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 asize=30parameter 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 - 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 - 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
- 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)
- Metaprogramming in Python seems a little quirky. For instance, adding to the
__metaclasses__variable as a means of doing a Mixin is quirky. TheIncludemechanism 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
- 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 - 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
- Even with Mongrel it __feels__ 33% faster than Rails. I have no benchmarks to support this, just my general impressions
- 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_assignmentso 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 - 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
- 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
breakpointmethod - 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 - 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
- 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 - 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
- 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
- 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
- Rails spits out directories all over the place -
components, vendor, lib, script, test config, dbetc... 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 - Naming a file
projects_controller, and the resulting classProjectsControllerseems weird to me - 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
- 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
- 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
- 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
- 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 aDocumentclass. Classes likeHandbook,Formetc... the package OJB was able to make a class hierarchy of these, store a generated key in aojb_hl_seqtable - 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 classDocumentor 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' endMaybe I'm weird, but this seems fairly easy. I don't totally understand how thebefore_createis 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