Ian Bicking: the old part of his blog

Magic and Backtracing Code

SQLObject actually has, in its history, a great deal of similarity to other Python ORMs. Not just the whole wraps-a-database-thing (which it obviously should have in common), but little implementation details. For instance, like PyDO and Django, it used to have a list of columns (instead of using attribute assignment). All of those projects have changed since then... and probably in a similar way you can lingering artifacts of that past implementation detail.

One of the ways is how the class is actually constructed. This history often reflects a past when a class was a mostly-dumb holder of a data definition. Then some outside code (the ORM itself) looks at the class definition for special attributes, and constructs a bunch of stuff. So, for instance, though you do name = StringCol() in SQLObject, StringCol is just a description of the column. It doesn't actually do anything, and if you later fetch MyClass.name you won't get back anything related to StringCol. Because what is actually happening is that those descriptions are collected, then the class is built.

This is something I'm trying to move away from in SQLObject, and I think 0.8 will have some significant progress here. One of the goals of that progress is to make a distinction between SQLObject and ActiveRecord, (and, less direction, from Django's ORM). Because -- admitting that this is a judgemental and subjective term -- I want SQLObject to be the most Pythonic of these options. Where Pythonic doesn't just mean fits-the-language (can't expect a Ruby library to want to fit into Python) but is a more generic term for Everything That Is Good In Programming.

In this case, there's a specific feature of Python I want to maintain: backtracking. Python's namespaces and tendency towards functions and imperative code means that it's fairly easy, given a local bit of code, to figure out what that code is doing in terms of the larger system. You can read code inside out, instead of having to figure everything out up front. Metaprogramming on the whole tends to break that, because you don't even understand the dialect of code you are reading, not to mention where methods are implemented and what side effects they might have. So SQLObject already breaks backtracking; my goal is to mitigate that.

One instance is joins, which have some annoying surprises in SQLObject exactly because of the legacy of treatment of classes as declaration. I have a refactoring of joins (not yet checked in) that will hopefully clear things up and generally simplify things. Over on the TurboGears list people wanted syntax similar to Django's for adding instances, and it would have meant:

class Person(SQLObject):
    addresses = MultipleJoin('Address')
p = Person.get(1)
p.addAddress(street='123 W 12th', ...)

I.e., the presence of that join would cause the addAddress method to be created. Seeing addAdress in code, how would you figure out what that did? Well, you'd just have to be familiar with how the code works, because addAddress simply won't exist in any other fashion. But that's what I want to get away from; what will actually go into SQLObject will be:

class Person(SQLObject):
    addresses = OneToMany('Address')
p = Person.get(1)
p.addresses.create(street='123 W 12th', ...)

If you want to know what's going on, you look up OneToMany.create(). Well... sadly it won't be that easy, as OneToMany is a descriptor, and it actually returns an object that delegates to SelectResults. There's still a lot to keep track of, and the challenge I'll have is to figure out a way to present a wider set of core concepts than is currently in the documentation (for instance, SelectResults is a really important class, but it's not documented and it's always instantiated for you). I'm thinking SQLObject docs should move towards a casual overview, with deep links into generated documentation that is more complete.

Created 01 Nov '05


Why not treat the one-to-many join as what it naturally returns? A list. Let the user make natural Python list operations on the join and perform the necessary magic in the background. Rails ActiveRecord has a similar approach, and use Ruby list operators to deal with it to an extent.

It seems very natural and Pythonic to use Python list operators to deal with results that for all appearences are a list (though un-ordered).

# Ben Bangert

I'll agree in general that using familiar list constructs is good.

Ian's example above is actually something that there is no Python list equivalent for: "create" would create a new instance (with the provided parameters, plus a parameter for the join column) and add it to the list. This is not the same as a simple "append".

# Kevin Dangoor

ManyToMany joins will use the set-like methods .add() and .remove(). The fact that Set uses .add() is the specific reason I named the method I showed as create and not add. It's certainly my intention to make these recognizable to the degree possible.

# Ian Bicking

Also, they are still lazily evaluated. So you can apply methods like .filter(extra_clauses) or .orderBy(column) and these are evaluated in the database. I think -- but still need to give it some thought -- that it will be good for "natural" joins as well. E.g., if there's a ManyToMany join between Book and Author, you might do Author.select(Author.books & Book.title.startswith('L')) to select all authors that have books with titles that start with L -- Author.books would evaluate (in that context) to the SQL join. So that would kind of address a separate request at the same time.

# Ian Bicking

Just tripped over this. Ian, I don't think you want to move way from what Active Record. It sounds like you want to embrace it:

class Person < ActiveRecord::Base
  has_many :addresses

p = Person.find(1)
p.addresses.create(:street => "123 W 12th")

It's not too late, Ian. You're still much welcome in the Ruby camp. Ryan Tomayko is already finding himself more than comfortably accustomed :)

# David Heinemeier Hansson

Ohhhhhh. This is just what I need right now. Thanks. :)

# Ryan Tomayko