StrataFrame Forum

Unexpected behavior during BO Save-on-Transaction

http://forum.strataframe.net/Topic30915.aspx

By Kirk M Sherhart - 2/10/2012

I've come across some unexpected behavior when using bo.Save(true) (i.e. BO-level saving, not form-level saving)

If I have a simple saving method as follows:

void SaveProject() {
    BusinessLayer.TransactionBegin("", IsolationLevel.ReadCommitted);
    SaveUndoResult result = bo.Save(true);
    if (result == SaveUndoResult.Success) {
        BusinessLayer.TransactionCommit("");
    }
    else {
        BusinessLayer.TransactionRollback("");
    }
}


Let's suppose I call SaveProject(), but the bo fails its CheckRules.  Therefore, the save operation fails and the transaction is rolled-back, as expected.  However, the internal value of bo._IsSavedOnTransaction is set to true by bo.Save function, even though the rules checked fail. (Note: the call to BusinessLayer.TransactionRollback does not reset the value bo._IsSavedOnTransaction to false.)

Now, if I fix the cause of the rule failure and call ProjectSave() a second time, nothing happensAngry Why? Because the value bo._IsSavedOnTransaction is true from the first call, and bo.IsDirty (called in the bo.Save() routine) returns false.  Therefore, no save is attempted.

I don't know if this is a bug or a feature.  At least, it's unexpected behavior.
By Edhy Rijo - 2/10/2012

Hi Kirk,
Not sure if this will fix your issue, but it is always good to use a Transaction Key value for your transactions, so try your code adding a TransactionKey parameter to the following methods:

BO.Save(True, "MyTransactionKeyValue")
BusinessLayer.TransactionCommit("","MyTransactionKeyValue")
BusinessLayer.TransactionRollback("","MyTransactionKeyValue").
By Larry Caylor - 4/9/2012

I'm having a similar issue. I hadn't run into this before since I haven't made heavy use of transactions. But now I have two objects on a form that I need to keep in sync so I wrapped the save by form in a transaction. Everything is fine, unless the transaction fails and is rolled back. In my case the error was a foreign key constraint. Once the the transaction is rolled back the IsDirty state on one of my objects is complety messed up and the only way out is to refill the object.

If I remove the transaction for the save it will still fail but the IsDirty state is correct. I haven't looked through the SF code yet but it sounds like it might be related to the following post. Did anyone ever verify if this is an issue? http://forum.strataframe.net/Topic29628.aspx?Keywords=IsDirty

-Larry
By Larry Caylor - 4/10/2012

After taking a look at the BusinessLayer code I think I understand what it going on. Transaction.Rollback only rolls back transactions at the database level. When you have multiple business objects in a transaction, objects that have been saved but not committed prior to an exception being thrown are not rolled back leaving the SavedOnTransactionFlags set. Normally business rules should prevent the transaction from failing in the first place, but if it does the only way I’ve  found to recover is to create a new instance of the objects that were save but not committed and refill them.  Is there any other way to reset an object that has been saved but not committed without creating a new instance?
By Edhy Rijo - 4/10/2012

Hi Larry,
I have not look at the source code, but if the transaction fails and in your case, because a foreign key was not assigned, the End User would not have a way to recover from that issue.  If in fact this issue is a bug and you need to force the BO.IsDirty back to True, simply call the BO.Edit() and that should take care of the IsDirty issue.
By StrataFrame Team - 4/10/2012

You're right, guys, that's a bug.  I've never run into it before because we always use the TransactionCommit() and TransactionRollback() calls within a Try/Catch.  If the TransactionCommit() throws an exception, then the TransactionRollback() is called to clean up any records that failed to insert together.

Inside the TransactionCommit() method, there is a call to ResetIsSavedOnTransactionFlagOnBusinessObjects() which resets that flag you're talking about.  Since we always call TransactionCommit() and then only call TransactionRollback() in the Catch block, we've never run into your issue.  

In case you're wondering, the _IsSavedOnTransaction flag is used to prevent multiple saves on business objects when you save on a transaction.  The scenario this prevents goes like this:

1) Business object A is the parent
2) Business object B is the child
3) You call Save() on A, but since it's saved on a transaction, the AcceptChanges() is not called on the table until the commit, so it stays dirty, even though the records have been inserted/updated/deleted.
4) You call Save() on B, which checks to make sure it's parent has been saved first, which it has, but without the _IsSavedOnTransaction flag, the child thinks the parent is still dirty, so it gets saved again

I have modified the TransactionRollback() root overload as follows to reset the _IsSavedOnTransaction flag for those of you who like to call TransactionRollback() without first calling TransactionCommit().



        ''' <summary>
        ''' Rolls back the transaction that is tied to all business objects associated with the given database
        ''' (All business objects that have the same database name use the same transaction.)
        ''' </summary>
        Public Shared Sub TransactionRollback(ByVal DataSourceKey As String, ByVal TransactionKey As String)
            '-- 4/10/2012 - Ben
            '-- Added this call to reset the flag on the business objects so they can remain dirty after 
            '   the transaction is rolled back without a call to TransactionCommit() first
            ResetIsSavedOnTransactionFlagOnBusinessObjects(DataSourceKey, TransactionKey)
            '-- http://forum.strataframe.net/Topic30915.aspx

            DataLayer.TransactionRollback(DataSourceKey, TransactionKey)
        End Sub

The code starts on line 6250 in the BusinessLayer.vb file.  You can modify it yourself and recompile.  If you don't want to do that, you're other options would be to wait for the next build, or *hushed voice* call ResetIsSavedOnTransactionFlagOnBusinessObjects() through reflection */hushed voice*.

Hope that helps, and thanks for the catch.

Ben 
By Edhy Rijo - 4/10/2012

Hi Ben,
Welcome back!!!! Tongue

Thanks for the explanation, I am sure Larry will try to implement the change.  In my case I use transactions the same way you do and never hit that issue.
By Larry Caylor - 4/10/2012

Hi Ben,

Thanks for the info, I'll give it a try. There is one other little problem in the code.  When TransactionRollback, line 6241,  is called without a transaction key (default key) it is calling TransactionRollback on the DataLayer instead of BusinessLayer so it would bypass the root overload that you just modified.

-Larry


''' <summary>        
''' Rolls back the transaction that is tied to all business objects associated with the given database
       
''' (All business objects that have the same database name use the same transaction.)       
''' </summary>       
Public
 Shared Sub TransactionRollback(ByVal DataSourceKey As String)  -->     
DataLayer
.TransactionRollback(DataSourceKey, DEFAULT_TRANSACTIONKEY)       
End
 Sub
By StrataFrame Team - 4/10/2012

Good catch, Larry.

I'll change it to call the other overload.  Thanks.
By Larry Caylor - 4/10/2012

Thanks for the quick fix. I made the changes and recompiled and it works like a champ.

-Larry
By Keith Chisarik - 9/27/2013

Has this been fixed?