[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 |