views:

486

answers:

5

I do not know how to implement undo property of user-friendly interfaces using a transactional database.

On one hand it is advisable that the user has multilevel (infinite) undoing possibility as it is stated here in the answer. Patterns that may help in this problem are Memento or Command.

However, using a complex database including triggers, ever-growing sequence numbers, and uninvertable procedures it is hard to imagine how an undo action may work at different points than transaction boundaries. In other words, undo to a point when the transaction committed for the last time is simply a rollback, but how is it possible to go back to different moments?

UPDATE (based on the answers so far): I do not necessarily want that the undo works when the modification is already committed, I would focus on a running application with an open transaction. Whenever the user clicks on save it means a commit, but before save - during the same transaction - the undo should work. I know that using database as a persistent layer is just an implementation detail and the user should not bother with it. But if we think that "The idea of undo in a database and in a GUI are fundamentally different things" and we do not use undo with a database then the infinite undoing is just a buzzword. I know that "rollback is ... not a user-undo".

So how to implement a client-level undo given the "cascading effects as the result of any change" inside the same transaction?

+1  A: 

Some (all?) DBMSs support savepoints, which allow partial rollbacks:

savepoint s1;
insert into mytable (id) values (1);
savepoint s2;
insert into mytable (id) values (2);
savepoint s3;
insert into mytable (id) values (3);
rollback to s2;
commit;

In the above example, only the first insert would remain, the other two would have been undone.

I don't think it is practical in general to attempt undo after commit, for the reasons you gave* and probably others. If it is essential in some scenario then you will have to build a lot of code to do it, and take into account the effects of triggers etc.

  • I don't see any problem with ever-increasing sequences though?
Tony Andrews
Depending on the transaction isolation level and amount of data shared between clients, this could lead to some pretty nasty locking issues.
Jesse Weigert
Jesse, do you mean that rolling back to a savepoint could lead to nasty locking issues? I don't see how, but then I use Oracle.
Tony Andrews
+4  A: 

The idea of undo in a database and in a GUI are fundamentally different things; the GUI is going to be a single user application, with low levels of interaction with other components; a database is a multi-user application where changes can have cascading effects as the result of any change.

The thing to do is allow the user to try and apply the previous state as a new transaction, which may or may not work; or alternatively just don't have undos after a commit (similar to no undos after a save, which is an option in many applications).

William
Generally I do not want to go back before a commit. My main point was to go back to a point _after_ the last commit. Since then lots of db manipulations might happen. that are hard to undo one by one.
rics
You could allow people to reset the form to the previous state and then try and apply that as a new transation; trying to implement undos in a database is very difficult, and almost certainly more effort than it's worth - you have no idea what other users might have done to the data, for example
William
What if one user has more than one open form on the same transaction? Rolling back and starting a new transaction also hurts the other forms.
rics
"What if one user has more than one open form on the same transaction?" - DON'T allow that situation to arise: if the forms are supposed to be independent then they need their own database sessions.
Tony Andrews
I did not say that they are independent. If there is a dependence then they need to be on the same form (e.g. customers, orders)?
rics
A: 

We developped such a possibility in our database by keeping track of all transactions applied to the data (not really all, just the ones that are less than 3 months old). The basic idea was to be able to see who did what and when. Each database record, uniquely identified by its GUID, can then be considered as the result of one INSERT, multiple UPDATEs statements, and finally one DELETE statement. As we keep tracks of all these SQL statements, and as INSERTs are global INSERTS (a track of all fields value is kept in the INSERT statement), it is then possible to:

  • Know who modified which field and when: Paul inserted a new line in the proforma invoice, Bill renegociated the unit price of the item, Pat modified the final ordered quantity, etc)
  • 'undo' all previous transactions with the following rules:

    an 'INSERT' undo is a 'DELETE' based on the unique identifier

    an 'UPDATE' undo is equivalent to the previous 'UPDATE'

    a 'DELETE' undo is equilavent to the first INSERT followed by all updates

  • As we do not keep tracks of transactions older than 3 months, UNDO's are not allways available.

Access to these functionalities are strictly limited to database managers only, as other users are not allowed to do any data update outside of the business rules (example: what would be the meaning of an 'undo' on a Purchase Order line once the Purchase Order has been agreed by the supplier?). To tell you the truth, we use this option very rarely (a few times a year?)

Philippe Grondier
It is all about logging as it is discussed here: http://stackoverflow.com/questions/195410/how-to-log-in-an-oracle-database. I would like a solution that works during one run of the application.
rics
+1  A: 

It's nearly the same like William's post (which I actually voted up), but I try to point out a little more detailed, why it is neccessary to implement a user undo (vs. using a database rollback).

It would be helpful to know more about your application, but I think for a user(-friendly) Undo/Redo the database is not the adequate layer to implement the feature.

  1. The user wants to undo the actions he did, independent of if these lead to no/one/more database transactions
  2. The user wants to undo the actions HE did (not anyone else's)

The database from my point of view is implementation detail, a tool, you use as a programmer for storing data. The rollback is a kind of undo that helps YOU in doing so, it's not a user-undo. Using the rollback would mean to involve the user in things he doesn't want to know about and does not understand (and doesn't have to), which is never a good idea.

As William posted, you need an implementation inside the client or on server side as part of a session, which tracks the steps of what you define as user-transaction and is able to undo these. If database transactions were made during those user transactions you need other db transactions to revoke these (if possible). Make sure to give valuable feedback if the undo is not possible, which again means, explain it business-wise not database-wise.

Kai
I know that a database as persistent storage is an implementation detail. This is what I am trying to hide from the users with implementing undo and not just giving the possibility to commit with save and rollback with cancel. But can I undo an update what calls a stored procedure via a trigger?
rics
A: 

To maintain arbitrary rollback-to-previous semantics you will need to implement logical deletion in your database. This works as follows:

Each record has a 'Deleted' flag, version number and/or 'Current Indicator' flag, depending on how clever you need your reconstruction to be. In addition, you need a per entity key across all versions of that entity so you know which records actually refer to which specific entity. If you need to know the times a version was applicable to, you can also have 'From' and 'To' columns.

When you delete a record, you mark it as 'deleted'. When you change it, you create a new row and update the old row to reflect its obsolescence. With the version number, you can just find the previous number to roll back to.

If you need referential integrity (which you probably do, even if you think you don't) and can cope with the extra I/O you should also have a parent table with the key as a placeholder for the record across all versions.

On Oracle, clustered tables are useful for this; both the parent and version tables can be co-located, minimising the overhead for the I/O. On SQL Server, a covering index (possibly clustered on the entity key) including the key will reduce the extra I/O.

ConcernedOfTunbridgeWells