On 01/20/2017 01:45 PM, Philip Scott wrote:
Hello all,

Short summary of my question:

Is it possible to stop rollback() from expiring dirty objects?

Specifically, in the case where there are
 - local changes made outside to mapped objects outside of a transaction
 - flush() is called
 - the flush fails

In this case SQLAlchemy will expire any objects that had local changes,
and those changes are lost. I would ideally like get objects & session
to be in the same state as they were before I called flush().

Longer explanation of my approach, in case I am just doing it wrong :)

I have a GUI application where users can load, view, and edit some
database mapped objects. Stripped down, it's pretty simple stuff like this:

class Action(Base):
    __tablename__      = "action"
    id                 = Column(Integer, primary_key=True)
    description        = Column(Text)

class Step(Base, ActionSchemaMixin):
    __tablename__      = "step"
    id                 = Column(Integer, primary_key=True)
    id_action          = Column(Integer, ForeignKey(Action.id))

    action             = relationship(Action, backref='steps')

So the action has some attributes, and a number of steps, which have
their own attributes. Users can select an action (or create a new one) -
then add, remove, and update steps for that action from the GUI.

I have found it to be an extremely useful pattern to couple these
widgets directly to the mapped objects they are dealing with, and let
the various event handlers to update the mapped objects whenever the
user alters the GUI.

The following pseudo-code should give you an idea

class ActionWidget(Widget):
    def __init__(action):
        self.action = action
        self.description_edit_widget =
TextEditWidget(on_changed=self.on_description_widget_changed)

     def on_description_widget_changed(self, new_description):
        self.action.description = new_description

I also have a widget for a list-of-steps that shows a summary for each,
and allows you to add/remove new ones - and a widget for editing an
individual step.

Because I have no control over how long the user will faff about when
editing one of these actions, and we do not want long open transactions
to the database, I have set up my session with the slightly
controversial (autoflush=False, autocommit=True, expire_on_commit=False)
options. I load up the action and all the steps, the user can take as
long as they like mucking about with it - and when they press the 'Save'
button, I just flush all the changes.

It works great, except if something goes wrong with the flushing. If
some DB constraint is violated for example, the transaction created in
flush() is rolled back. All my modified objects are expired, and any new
ones are expunged, as faithfully described in the docs.

In this case though, it is unfortunate, because now I have a mismatch
between what is in my GUI and the objects they are supposed to be
displaying. I want to be able to warn my user about the problem and let
them fix the one field that was wrong, without forcing them to loose all
their changes and start again.

It is quite difficult for me to recreate the mapped objects changed
state from the GUI elements alone, as there are temporary widgets
created at times to edit certain attributes.

The only way around this that I can think of is to detach all the
objects before coupling them to the GUI elements - then in my save
function, somehow copying all of the updated/new objects before adding
them to a session and attempting to commit the changes. I could get this
to work, but think it might be fiddly when there are a mixture of new
and dirty persisted objects.

Interestingly when creating a set of brand new objects, things are fine
too - although they are expunged, they can be re-added to the session
when the user is ready to try again.

hoping there is a simple answer but suspecting there isn't :)

so the quick and dirty approach is a flag I'm not enthused about, nevertheless I don't plan on removing, called _enable_transaction_accounting, that when set to False, will put the Session in total SQLAlchemy 0.4 mode where there is no tracking or expiry of state across transactions of any kind. This flag is in the docs for Session, it goes alongside the autocommit etc. flags.

The more architectural answer is that the ORM considers these objects to be proxies for state within a database transaction. That your GUI lives outside of the span of a single transaction is correct, but also implies that the pattern to persist the GUI to the database would involve copying its state into the database structure, which in this case is your ORM model. That is, the state of your GUI would in fact be represented outside of those ORM objects.

You can do this kind of pattern by having the ORM objects represent the GUI state as "detached" objects, then copy the state of those objects into the session using a method like Session.merge. On an empty Session this would incur a lot of SELECT statements, but you can try working with session.merge(..., load=False) and feed the objects in, if they have pending changes on them themselves you can use sqlalchemy.orm.session.make_transient_to_detached() on each to set them as "clean".

The best way would be that the GUI has its own dedicated data structure that is distinct from your ORM model. A more high-end way of representing this is to create "action" objects which represent things the user does, like pressing a button or selecting a choice in a dropdown. You log these "action" objects into a list as the user does them, and when ready for persist, you "play" these objects into an ORM model. This in formal parlance is known as the "command" pattern and one reason it's key for GUIs is that it gives you "undo" ability for free - each Command object can not only "play" itself into the GUI, it can also "unplay" itself. Overall, the idea is not to look at the GUI as a current set of state, look at it as the graphical representation of a series of events that have occurred.

So you have "quick", "better", and "formalized" options on this.





All the best,

Philip

--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and
Verifiable Example. See http://stackoverflow.com/help/mcve for a full
description.
---
You received this message because you are subscribed to the Google
Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send
an email to [email protected]
<mailto:[email protected]>.
To post to this group, send email to [email protected]
<mailto:[email protected]>.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

--
SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to