Thread: [SQLObject] SQLObject's inheritance patch
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
From: Daniel S. <sql...@xs...> - 2004-01-15 22:50:08
|
--- SQLObject.orig/SQLObject.py 2003-11-12 12:03:45.000000000 -0500 +++ SQLObject/SQLObject.py 2004-01-14 23:50:50.000000000 -0500 @@ -4,6 +4,10 @@ SQLObject is a object-relational mapper. See SQLObject.html or SQLObject.txt for more. +Modified by + Daniel Savard, Xsoli Inc <sqlobject xsoli.com> 14 Jan 2004 + - Added support for simple table inheritance. + This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the @@ -155,10 +159,37 @@ classRegistry[registry] = {} classRegistry[registry][className] = newClass + #DSM: Need to keep the name of the class for easy access later + newClass._className = className + #DSM: Need to know very soon if the class is a children of an inheritable class. + #DSM: If so, we keep a link to our parent class. + for cls in bases: + if hasattr(cls, '_inheritable') and cls._inheritable: + newClass._parentClass = cls + # We append to _columns, but we don't want to change the # superclass's _columns list, so we make a copy if necessary if not d.has_key('_columns'): newClass._columns = newClass._columns[:] + #DSM: If this class is a child of a parent class, we need to do some + #DSM: attribute check and a a foreign key to the parent. + if newClass._parentClass: + #DSM: First, look for invalid column name: reserved ones or same as a parent + parentCols = [column.kw['name'] for column in newClass._columns] + for col in implicitColumns: + cname = col.kw['name'] + if cname in ['parent', 'parentID', 'childID', 'childName']: + raise AttributeError, "The column name '%s' is reserved" % cname + if cname in parentCols: + raise AttributeError, "The column '%s' is already defined in an inheritable parent" % cname + #DSM: Remove columns if inherited from an inheritable class as we don't want them + #DSM: All we want is a foreign key that will link to our parent + newClass._columns = [] + newClass._columns.append(Col.ForeignKey(name='parent', foreignKey=newClass._parentClass._className)) + #DSM: If this is inheritable, add some default columns to be able to link to children + if hasattr(newClass, '_inheritable') and newClass._inheritable: + newClass._columns.append(Col.IntCol(name='childID',default=None)) + newClass._columns.append(Col.StringCol(name='childName',default=None)) newClass._columns.extend(implicitColumns) if not d.has_key('_joins'): newClass._joins = newClass._joins[:] @@ -218,6 +249,13 @@ # SQL where-clause generation. See the sql module for # more. newClass.q = SQLBuilder.SQLObjectTable(newClass) + #DSM: If we are a child, get the q magic from the parent + currentClass = newClass + while currentClass._parentClass: + currentClass = currentClass._parentClass + for col in currentClass._columns: + if type(col) == Col.ForeignKey: continue + setattr(newClass.q, col.kw['name'], getattr(currentClass.q, col.kw['name'])) for column in newClass._columns[:]: newClass.addColumn(column) @@ -349,6 +387,14 @@ # it's set (default 1). _cacheValues = True + #DSM: The _inheritable attribute controls wheter the class can by + #DSM: inherited 'logically' with a foreignKey and back reference. + _inheritable = False # Does this class is inheritable + _parentClass = None # A reference to the parent class + parent = None # Foreign key to the parent + childID = None # Id to the children + childName = None # Children name (to be able to get a subclass) + # The _defaultOrder is used by SelectResults _defaultOrder = None @@ -442,7 +488,7 @@ # Here if the _get_columnName method isn't in the # definition, we add it with the default # _SO_get_columnName definition. - if not hasattr(cls, getterName(name)): + if not hasattr(cls, getterName(name)) or name == 'parentID' or name == 'childID' or name == 'childName': setattr(cls, getterName(name), getter) cls._SO_plainGetters[name] = 1 @@ -459,7 +505,7 @@ setattr(cls, '_SO_fromPython_%s' % name, column.fromPython) setattr(cls, rawSetterName(name), setter) # Then do the aliasing - if not hasattr(cls, setterName(name)): + if not hasattr(cls, setterName(name)) or name == 'parentID' or name == 'childID' or name == 'childName': setattr(cls, setterName(name), setter) # We keep track of setters that haven't been # overridden, because we can combine these @@ -488,10 +534,19 @@ # And we set the _get_columnName version # (sans ID ending) - if not hasattr(cls, getterName(name)[:-2]): + if not hasattr(cls, getterName(name)[:-2]) or name == 'parentID': setattr(cls, getterName(name)[:-2], getter) cls._SO_plainForeignGetters[name[:-2]] = 1 + #DSM: Try to add parent properties to the current class + if name=='parentID': + for col in cls._parentClass._columns: + cname = col.kw['name'] + if cname == 'parent' or cname == 'childID' or cname == 'childName': continue + setattr(cls, getterName(cname), eval('lambda self: self.parent.%s' % cname)) + if not col.kw.has_key('immutable') or not col.kw['immutable']: + setattr(cls, setterName(cname), eval('lambda self, val: setattr(self.parent, %s, val)' % repr(cname))) + if not column.immutable: # The setter just gets the ID of the object, # and then sets the real column. @@ -835,6 +890,18 @@ else: id = None + #DSM: If we were called by a children class, we must retreive the properties dictionnary. + #DSM: Note: we can't use the ** call paremeter directly as we must be able to delete items from + #DSM: the dictionary (and our children must know that the items were removed!) + fromChild = False + if kw.has_key('kw'): + kw = kw['kw'] + fromChild = True + #DSM: If we are the children of an inheritable class, we must first create our parent + if cls._parentClass: + parent = cls._parentClass.new(kw=kw) + kw['parent'] = parent + # First we do a little fix-up on the keywords we were # passed: for column in inst._SO_columns: @@ -864,7 +931,10 @@ for name, value in kw.items(): if name in inst._SO_plainSetters: forDB[name] = value - else: + #DSM: If this is a call from the child, we must remove the parameter for the database + if fromChild: del kw[name] + elif not fromChild: + #DSM: Only use other items if this isn't a call from the child others[name] = value # We take all the straight-to-DB values and use set() to @@ -881,9 +951,21 @@ # Then we finalize the process: inst._SO_finishCreate(id) + + #DSM: If we are a child, we must set our parent link + if cls._parentClass: + parent.childID = inst.id + parent.childName = inst._className + return inst new = classmethod(new) + #DSM: return the subclass if asked for + def getSubClass(self): + if not hasattr(self, 'childID'): return None + if self.childID is None: return None + return findClass(self.childName)(self.childID) + def _SO_finishCreate(self, id=None): # Here's where an INSERT is finalized. # These are all the column values that were supposed @@ -1008,6 +1090,13 @@ clearTable = classmethod(clearTable) def destroySelf(self): + #DSM: If this object has children, find the last one and destroy it. If not, simply destroy this object + while self.getSubClass(): self = self.getSubClass() + self._destroySelf() + + def _destroySelf(self): + #DSM: If this object has parents, recursivly kill them + if self.parent: self.parent._destroySelf() # Kills this object. Kills it dead! self._SO_obsolete = True self._connection._SO_delete(self) @@ -1066,6 +1155,33 @@ self.clause = clause tablesDict = SQLBuilder.tablesUsedDict(self.clause) tablesDict[sourceClass._table] = 1 + #DSM: if this class has a parent, we need to link it and be sure the parent is in the table list + #DSM: the following code is before clauseTables because if the user uses clauseTables (and normal string SELECT), he + #DSM: must know what he wants and will do himself the relationship between classes. + if type(self.clause) is not str: + tableRegistry = {} + for registryClass in classRegistry[None].values(): + if registryClass._table in tablesDict: + #DSM: By default, no parent are needed for the clauses + tableRegistry[registryClass] = registryClass + currentClass = registryClass + while currentClass._parentClass: + currentClass = currentClass._parentClass + if tableRegistry.has_key(currentClass): + #DSM: Must keep the last parent needed (to limit the number of join needed) + tableRegistry[registryClass] = currentClass + #DSM: Remove this class as it is a parent one of a needed children + del tableRegistry[currentClass] + #DSM: Table registry contains only the last children or standalone classes + parentClause = [] + for (currentClass, minParentClass) in tableRegistry.items(): + while currentClass != minParentClass and currentClass._parentClass: + parentClass = currentClass._parentClass + parentClause.append(currentClass.q.parentID == parentClass.q.id) + currentClass = parentClass + tablesDict[currentClass._table] = 1 + self.clause = reduce(SQLBuilder.AND, parentClause, clause) + if clauseTables: for table in clauseTables: tablesDict[table] = 1 |
From: Ian B. <ia...@co...> - 2004-01-20 16:57:03
|
Daniel Savard wrote: > Here is a patch that add simple inheritance to SQLObject 0.8.1. > If you try it, please send me some comments (to the list or my > email). Thanks. Thanks for contributing this. It looks like it required less modifications than I expected. One thing I wondered about: will this work with the addColumn class method? It doesn't seem like it -- the inheritance structure seems to be constructed entirely at the time of class instantiation. To do this, superclasses would need to know about all their subclasses, so they could add the column to subclasses as well. > A new class attribute '_inheritable' is added. When this new > attribute is set to 1, the class is marked 'inheritable' and two colomns > will automatically be added: childID (INT) and childName (TEXT). When a > class inherits from a class that is marked inheritable, a new colomn > (ForeignKey) will automatically be added: parent. Is childID needed? It seems like the child should share the parent's primary key. > The column parent is a foreign key that point to the parent class. > It works as all SQLObject's foreign keys. There will also be a parentID > attribute to retreive the ID of the parent class. > > The columns childID and childName will respectivly contain the id > and the name of the child class (for exemple, 1 and 'Employee'. This > will permit to call a new function: getSubClass() that will return a > child class if possible. These seem weird to me, but I think that's the disconnect between RDBMS inheritance (which is really just another kind of relationship), and OO inheritance. I'd rather see Person(1) return an Employee object, instead of having to use getSubClass(). Or, call getSubClass() simply "child", so that someEmployee.child.parent == someEmployee. But at that point, it really doesn't look like inheritance. Rather we have a polymorphic one-to-one (or one-to-zero) relation between Person and Employee, and potentially other tables (exclusive with the Employee relation). The functional difference is that the join is in some ways implicit, in that Employee automatically brings in all of Person's columns. At which point Employee looks kind of like a view. Could this all be implemented with something like: class Person(SQLObject): child = PolyForeignKey() # Which creates a column for the table name, and maybe one for the # foreign ID as well. class Employee(SQLObject): parent = ColumnJoin('Person') # ColumnJoin -- maybe with another name -- brings in all the columns # of the joined class. This seems functionally equivalent to this inheritance, but phrased as relationships. And, phrased as a relationship, it's more flexible. For instance, you could have multiple ColumnJoins in a class, without it being very confusing, and multiple PolyForeignKeys, for different classes of objects. Ian |