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