[SQLObject] SQLObject's inheritance patch version 2
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
From: Daniel S. <sa...@gn...> - 2004-02-07 15:49:35
|
--- SQLObject.orig/SQLObject.py 2003-11-12 12:03:45.000000000 -0500 +++ SQLObject/SQLObject.py 2004-02-07 00:57:58.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> 7 Feb 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 + newClass._childClasses = {} + #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 + cls._childClasses[className] = newClass + # 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 ['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 = [] + #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.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,13 @@ # 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 + _childClasses = {} # Reference to child classes + childName = None # Children name (to be able to get a subclass) + # The _defaultOrder is used by SelectResults _defaultOrder = None @@ -408,9 +453,29 @@ cache.put(id, cls, val) finally: cache.finishPut(cls) + #DSM: If this class has a child, return the child, if not + if hasattr(val, 'childName'): + childName = val.childName + if childName is not None: + return val._childClasses[childName](id) + #DSM: This class doesn't have a child... return this class return val - def addColumn(cls, columnDef, changeSchema=False): + def addColumn(cls, columnDef, changeSchema=False, childUpdate=False): + #DSM: Try to add parent properties to the current class + #DSM: Only do this once if possible at object creation and once for + #DSM: each new dynamic column to refresh the current class + if childUpdate or cls._parentClass: + for col in cls._parentClass._columns: + cname = col.kw['name'] + if 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 childUpdate: + makeProperties(cls) + return + column = columnDef.withClass(cls) name = column.name assert name != 'id', "The 'id' column is implicit, and should not be defined as a column" @@ -442,7 +507,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 == 'childName': setattr(cls, getterName(name), getter) cls._SO_plainGetters[name] = 1 @@ -459,7 +524,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 == 'childName': setattr(cls, setterName(name), setter) # We keep track of setters that haven't been # overridden, because we can combine these @@ -517,6 +582,11 @@ if cls._SO_finishedClassCreation: makeProperties(cls) + #DSM: Update each child class if needed and existing (only for new + #DSM: dynamic column as no child classes exists at object creation) + for c in cls._childClasses.values(): + c.addColumn(columnDef, childUpdate=True) + addColumn = classmethod(addColumn) def addColumnsFromDatabase(cls): @@ -563,11 +633,30 @@ cls._connection.delColumn(cls._table, column) if cls._SO_finishedClassCreation: - unmakeProperties(cls) + delattr(cls, name) + + #DSM: Update each child class if needed and delete properties for this column + for c in cls._childClasses.values(): + delattr(c, name) delColumn = classmethod(delColumn) - def addJoin(cls, joinDef): + def addJoin(cls, joinDef, childUpdate=False): + #DSM: Try to add parent properties to the current class + #DSM: Only do this once if possible at object creation and once for + #DSM: each new dynamic join to refresh the current class + if childUpdate or cls._parentClass: + for jdef in cls._parentClass._joins: + join = jdef.withClass(cls) + jname = join.joinMethodName + jarn = join.addRemoveName + setattr(cls, getterName(jname), eval('lambda self: self._parent.%s' % jname)) + if hasattr(join, 'remove'): setattr(cls, 'remove' + jarn, eval('lambda self,o: self._parent.remove%s(o)' % jarn)) + if hasattr(join, 'add'): setattr(cls, 'add' + jarn, eval('lambda self,o: self._parent.add%s(o)' % jarn)) + if childUpdate: + makeProperties(cls) + return + # The name of the method we'll create. If it's # automatically generated, it's generated by the # join class. @@ -615,6 +704,11 @@ if cls._SO_finishedClassCreation: makeProperties(cls) + + #DSM: Update each child class if needed and existing (only for new + #DSM: dynamic join as no child classes exists at object creation) + for c in cls._childClasses.values(): + c.addJoin(joinDef, childUpdate=True) addJoin = classmethod(addJoin) @@ -641,7 +735,11 @@ delattr(cls, 'add' + join.addRemovePrefix) if cls._SO_finishedClassCreation: - unmakeProperties(cls) + delattr(cls, meth) + + #DSM: Update each child class if needed and delete properties for this join + for c in cls._childClasses.values(): + delattr(c, meth) delJoin = classmethod(delJoin) @@ -835,6 +933,20 @@ 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: + #kw['childName'] = inst._className + inst._parent = cls._parentClass.new(kw=kw) + inst._parent.childName = inst._className + id = inst._parent.id + # First we do a little fix-up on the keywords we were # passed: for column in inst._SO_columns: @@ -864,7 +976,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 @@ -1008,6 +1123,8 @@ clearTable = classmethod(clearTable) def destroySelf(self): + #DSM: If this object has parents, recursivly kill them + if hasattr(self, '_parent') and self._parent: self._parent.destroySelf() # Kills this object. Kills it dead! self._SO_obsolete = True self._connection._SO_delete(self) @@ -1066,6 +1183,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.id == 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 |