On Jul 12, 2009, at 5:57 PM, Antoine Pitrou wrote:

>
> Ok, thanks. Then I want to ask: why the complication? It looks like a
> weird and difficult to grasp behaviour. Why doesn't a failed flush()
> leave the session in a consistent state (either rolled back or not,
> but not something in-between)?


here is why.

the usage of the session should look like this:


try:
      <use session>
      session.commit()
except:
     session.rollback()
finally:
     session.remove()  # optional, depends on use case

many things can cause a failure within the try/except besides  
flushes.  You should always have some kind of "framing" of your  
session operations so that connection and transaction resources have a  
definitive boundary, otherwise your application doesn't really have  
its usage of resources under control.  This is not to say that you  
need to put try/except blocks all throughout your application - on the  
contrary this would be a terrible idea.   You should architect your  
application such that there is one (or few) point(s) of "framing"  
around session operations.     The common example is a web framework  
where the above framing takes place in the BaseController class.

Now the second part.   The Session supports "framing" above within  
multiple levels.   Such as, suppose you had a decorator  
@with_session(), which did this:

def with_session(fn):
     def go(*args, **kw):
         session.begin(subtransactions=True)
         try:
             ret = fn(*args, **kw)
             session.commit()
             return ret
         except:
             session.rollback()
     return go


the above decorator begins a transaction if one does not exist  
already, and then commits it, if it were the creator.  The  
"subtransactions" flag means that if begin() were already called by an  
enclosing funciton, nothing happens except a counter is incremented -  
this counter is decremented when commit() is called and only when it  
goes back to zero does the actual COMMIT happen.  It allows this usage  
pattern:


@with_session
def one():
     # do stuff
     two()


@with_session
def two():
     # etc.

one() can call two(), or two() can be called by itself, and the  
@with_session decorator ensures the appropriate "framing" - the  
transaction boundaries stay on the outermost call level.   As you can  
see, if two() calls flush() which throws an exception and then issues  
a rollback(), there will *always* be a second rollback() performed by  
the decorator, and possibly a third corresponding to two levels of  
decorator.   If the flush() pushed the rollback() all the way out to  
the top of the stack, and then we said that all remaining rollback()  
calls are moot, there is some silent behavior going on there.  A  
poorly written enclosing method might suppress the exception, and then  
call commit() assuming nothing is wrong, and then you have a silent  
failure condition.   The main reason people get this error in fact is  
because they didn't write clean "framing" code and they would have had  
other problems down the road.

If you think the above use case is a little exotic, the same kind of  
thing comes into play if you want to SAVEPOINT- you might call  
begin_nested() several times, and the commit()/rollback() calls each  
resolve the most recent begin_nested().    The meaning of rollback()  
or commit() is dependent upon which enclosing block it is called, and  
you might have any sequence of rollback()/commit() in any order, and  
its the level of nesting that determines their behavior.

In both of the above cases, if flush() broke the nesting of  
transaction blocks, the behavior is, depending on scenario, anywhere  
from "magic" to silent failure to blatant interruption of code flow.

flush() makes its own "subtransaction", so that a transaction is  
started up regardless of the external transactional state, and when  
complete it calls commit(), or rollback() upon failure - but that  
rollback() corresponds to its own subtransaction - it doesn't want to  
guess how you'd like to handle the external "framing" of the  
transaction, which could be nested many levels with any combination of  
subtransactions and real SAVEPOINTs.    The job of starting/ending the  
"frame" is kept consistently with the code external to the flush(),  
and we made a decision that this was the most consistent approach.

>
> By the way, I solved my problem by doing the UPDATE with an ORM-less
> query, so that I can catch the failure without rolling back the
> transaction. Perhaps flush() should have an optional flag to avoid
> rolling back on errors?

It would be great if flush() could partially complete and then not  
roll back, however this is beyond its current capabilities since its  
internal bookkeeping would have to be modified such that it can be  
halted at any time and be exactly consistent with what's been flushed  
to the database.     While this is theoretically possible, the  
usefulness of the enhancement is greatly decreased by the fact that  
many database operations require a ROLLBACK in any case.  Postgres in  
particular has operations which, once failed, the transaction is not  
allowed to continue:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index  
"foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of  
transaction block

What SQLAlchemy offers that solves both issues is support of  
SAVEPOINT, via begin_nested().   Using begin_nested(), you can frame  
an operation that may potentially fail within a transaction, and then  
"roll back" to the point before its failure while maintaining the  
enclosing transaction.




--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To post to this group, send email to sqlalchemy@googlegroups.com
To unsubscribe from this group, send email to 
sqlalchemy+unsubscr...@googlegroups.com
For more options, visit this group at 
http://groups.google.com/group/sqlalchemy?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to