Hi! I applied the patch with an additional test - that the connection is not
a transaction and is in autocommit mode. A transaction or non-autocommit
connection must be rolled back after a exception but the place for the
rollback is application-dependent so I didn't include a rollback in the
code. Inside a transaction it's also usually impossible to remove the
parent row(s) after an error - the only thing that's allowed is a rollback.
Committed in the revisions 4101-4103 (branches 0.11, 0.12 ad the trunk).
Thank you!
On Mon, Dec 07, 2009 at 06:41:54PM +0200, Tom Coetser wrote:
> I am doing some tests with InheritableSQLobject and have run into a problem
> which I hope to get some feedback on.
>
> The problem is that when creating a new object based on an
> InheritableSQLObject class, and the creation of the child object fails after
> a new parent was created, the parent record will remain in the database
> without the relevant child.
>
> Here is a short example:
>
> ~~~~~~~~~~ file: inheritTest3.py ~~~~~~~~~~~~
> from sqlobject import *
> from sqlobject.inheritance import InheritableSQLObject
>
> __connection__ = "sqlite:/:memory:"
>
> class Vehicle(InheritableSQLObject):
> vid = StringCol(notNone=True, default=None, unique=True)
> wheels = IntCol()
>
> class Bicycle(Vehicle):
> seats = IntCol(notNone=True, default=None)
>
> class Car(Vehicle):
> doors = IntCol(notNone=True, default=None)
>
> def createTables():
> Vehicle.createTable()
> Bicycle.createTable()
> Car.createTable()
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>
> Here is a sample session showing the problem when trying to create a bicycle
> but only supplying the args required for the Vehicle part of the object:
>
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> In [1]: from inheritTest3 import *
>
> In [2]: createTables(True)
>
> In [3]: b = Bicycle(vid="id1", wheels=4)
> ERROR: An unexpected error occurred while tokenizing input
> The following traceback may be corrupted or invalid
> The error message is: ('EOF in multi-line statement', (22, 0))
>
> ---------------------------------------------------------------------------
> IntegrityError Traceback (most recent call last)
>
> /home/tomc/mobilereporter.repo/main/app/<ipython console> in <module>()
>
> /usr/lib/python2.5/site-packages/sqlobject/main.pyc in __init__(self, **kw)
> 1221 id = None
> 1222
> -> 1223 self._create(id, **kw)
> 1224
>
> .... snipping lots of traceback parts ............
>
> /usr/lib/python2.5/site-packages/sqlobject/sqlite/sqliteconnection.pyc in
> _executeRetry(self, conn, cursor, query)
> 187 raise DuplicateEntryError(msg)
> 188 else:
> --> 189 raise IntegrityError(msg)
> 190 except self.module.InternalError, e:
> 191 raise InternalError(ErrorMessage(e))
>
> IntegrityError: bicycle.seats may not be NULL
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>
> If you now supply all required args for the bicycle, using the same field
> values for the vid and wheels as before, a duplicate entry error is raised:
>
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>
> In [4]: b = Bicycle(vid="id1", wheels=4, seats=2)
> ERROR: An unexpected error occurred while tokenizing input
> The following traceback may be corrupted or invalid
> The error message is: ('EOF in multi-line statement', (22, 0))
>
> ---------------------------------------------------------------------------
> DuplicateEntryError Traceback (most recent call last)
>
> /home/tomc/mobilereporter.repo/main/app/<ipython console> in <module>()
>
> /usr/lib/python2.5/site-packages/sqlobject/main.pyc in __init__(self, **kw)
> 1221 id = None
> 1222
> -> 1223 self._create(id, **kw)
> 1224
> 1225 for func in post_funcs:
>
> .... snipping lots of traceback parts ............
>
> /usr/lib/python2.5/site-packages/sqlobject/sqlite/sqliteconnection.pyc in
> _executeRetry(self, conn, cursor, query)
> 185 msg = ErrorMessage(e)
> 186 if msg.startswith('column') and msg.endswith('not
> unique'):
> --> 187 raise DuplicateEntryError(msg)
> 188 else:
> 189 raise IntegrityError(msg)
>
> DuplicateEntryError: column vid is not unique
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>
> The duplicate entry is because of the parent vehicle record was created when
> the first bicycle creation failed:
>
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> In [5]: q = "SELECT * from vehicle"
>
> In [6]: Vehicle._connection.queryAll(q)
> Out[6]: [(1, 'id1', 4, 'Bicycle')]
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>
> So, the problem is that if a new child object is created, but fails after the
> parent has been created, the parent is left in tact in the database.
>
> I'm attaching a patch as a possible fix for this, but I am not sure under
> which other circumstances this patch will cause other problems. Maybe a flag
> is needed inside the block that creates the child (parentCreatedHere=True),
> and only if this flag is set, should the parent be deleted in exception if
> the child creation failed?
>
> I would appreciate if someone with more insight into this can look at the
> patch an modify where neccessary.
>
> BTW: This patch is against SQLObject 0.11.0.
>
>
>
> Cheers,
> Tom
> --- inheritance/__init__.py.orig 2009-12-07 14:47:31.000000000 +0000
> +++ inheritance/__init__.py 2009-12-07 16:35:39.000000000 +0000
> @@ -384,7 +384,19 @@
>
> id = self._parent.id
>
> - super(InheritableSQLObject, self)._create(id, **kw)
> + # TC: Create this record and catch all exceptions in order to destroy
> + # TC: the parent if the child can not be created.
> + try:
> + super(InheritableSQLObject, self)._create(id, **kw)
> + except:
> + # TC: If we are the child, destroy the parent
> + if self.sqlmeta.parentClass:
> + self._parent.destroySelf()
> + #TC: Do we need to do this??
> + self._parent = None
> + # TC: Reraise the original exception
> + raise
> +
>
> def _findAlternateID(cls, name, dbName, value, connection=None):
> result = list(cls.selectBy(connection, **{name: value}))
Oleg.
--
Oleg Broytman http://phd.pp.ru/ ph...@ph...
Programmers don't die, they just GOSUB without RETURN.
|