Transaction Management Extension for SQLAnywhere |
Ver. 1.0 Contributed by Sharon Weinstrom Buntz |
What
| Transaction Management Made Easy... SQLAnywhere TM ready to go... |
- n_tr will replace your n_tr
- n_tr_SQLAnywhere is your new SQLAnywhere extension, inherited from n_tr
- u_dw and f_begin_end_tran_code are only here for you to copy code from
Why
| Why? ... To manage your SQLAnywhere transactions well... The success of your project depends on this... |
This extension will make your database run smoothly, and your customers will be happier because of it. Not only will this prevent downtime for your database, but it will also ensure that your data will have more integrity.
The whole concept of client/server means sharing data among several users, making sure that everyone can share at the same time, and making sure that everyone plays well together!
And that is the purpose of transaction management... to make sure that everyone is sharing a common resource nicely. Contention and Consistency are the two key things we are interested in...
The communication between the Client and the Server is the most underestimated and misunderstood of all programming tasks in a multi-user Client Server environment. Handling the database traffic in a manner which maximizes concurrency and ensures consistency is critical to the success of any project. It is substantially more important than anything else a developer does inside PowerBuilder. Consistency: The ability to run all day without corrupting the database, losing work, or retrieving misleading data. This is a good thing.
Concurrency: The ability for multiple users to use the same database at the same time and still get fast responses. This is also a good thing.
by Frank Postle and Breck Carter, http://www.bcarter.com/syb1_d.htm
Also with this new extension, you get enhanced database error messaging and thorough Debug Spy messaging. The enhanced messages contain a lot of extra information about the error. This will help you, as well as your customers, to investigate and understand why a database error has occurred. You will know exactly what was happening so you can pinpoint the problem!
How
| How will this make your database run smoothly? ... Take a look at what we do behind the scenes for you... |
Prevent your database log from filling up (which then makes the DBA have to reboot the server)
Prevent contention among different users (preventing locks on your database which lead to deadlocks)
Prevent your users from losing their work because of database problems
Prevent you from having to code explicit COMMITs/ROLLBACKs everywhere after every retrieve, etc.
Make sure that a set of related updates either all happen, or none happen
This in turn, will prevent your tables from getting out of sync
Keep your data valid, logical and reliable
Not hold the database in suspense
Not hold up other users' updates
Not cause the database log file to fill up
Basically, Prevent your database from locking up
Scope
| This extension was built... |
When and Where
| In a nutshell, here is what this service will do when your window updates... |
| Plus this service really beefs up your database error message and your spy messages... Just look at everything it tells you... |
| Database Error Window Contents | Debug Spy Audit Trail |
...Contents of above Multi-Line Edit follow below...
|
/*** Setting AutoCommit to FALSE (just for the duration of the transaction), using SQLCA -
Peims+Codes+Messages+Security Database (ClassName = n_tr_sqlanywhere) TimeStamp: 19/10/97
00:06:08:44 ***/ /*** INITIALIZING DATABASE ERROR MESSAGE TO <empty string>, using SQLCA - Peims+Codes+Messages+Security Database (ClassName = n_tr_sqlanywhere) TimeStamp: 19/10/97 00:06:08:55 ***/ /*** Setting TransInProgress flag to TRUE (since a transaction is beginning), using SQLCA - Peims+Codes+Messages+Security Database (ClassName = n_tr_sqlanywhere) TimeStamp: 19/10/97 00:06:08:60 ***/ /*** Update - dw_staff_maintenance(1) TimeStamp: 19/10/97 00:06:08:93 ***/ UPDATE "pf_staff_demo" SET "no_days_empld" = ? WHERE "name_f" = ? AND "gen" = ? AND "sex" = ? AND "ethn" = ? AND "no_days_empld" = ? AND "pct_day_empld" = ? AND "dob" = ? AND "prior_staff_id" = ? AND "demo_rev_confirm" = ? AND "staff_id" = ? AND "name_m" = ? AND "name_l" = ? /*** Update - dw_staff_maintenance(1) TimeStamp: 19/10/97 00:06:09:09 ***/ UPDATE "pf_staff_experi" SET "tot_yrs_experi" = ?, "yrs_experi_in_dist" = ? WHERE "tot_yrs_experi" = ? AND "high_deg" = ? AND "yrs_experi_in_dist" = ? AND "staff_id" = ? /*** Update - dw_staff_payroll(3) TimeStamp: 19/10/97 00:06:09:26 ***/ UPDATE "rsccc"."pf_staff_pay" SET "pay_amt" = ? WHERE "pay_acty" = ? AND "fund" = ? AND "func" = ? AND "obj" = ? AND "org" = ? AND "fscl_yr" = ? AND "pgm_int" = ? AND "pay_amt" = ? AND "staff_id" = ? /*** SETTING DATABASE ERROR MESSAGE TO A database error has occurred. DATABASE ERROR CODE: -157 <omissions - See
info to the left>, using SQLCA - Peims+Codes+Messages+Security Database
(ClassName = n_tr_sqlanywhere) TimeStamp: 19/10/97 00:06:09:31 ***/ <omissions - See info to the left>, using SQLCA - Peims+Codes+Messages+Security Database (ClassName = n_tr_sqlanywhere) TimeStamp: 19/10/97 00:07:36:54 ***/ |
| Tip: To avoid the ?s in your SQL messages
(where the data would be), DbParm=ConnectString='DSN=PFC;UID=dba;PWD=sql',DisableBind=1 This causes the database driver to not use memory
mapped variables for SQL. However, remember to turn this off for
production applications. |
Note: After this completes, none of the updates will be on the database anymore. All changes were rolled back just as soon as one update failed. |
Installation - Step by Step
| Follow these seven easy steps to install this extension... |
- Copy n_tr and n_tr_SQLAnywhere to your work PBL
- Regenerate n_tr, then n_tr_SQLAnywhere, then your application object (since n_tr has changed)
- In your application object, change the Variable Type for SQLCA from n_tr to n_tr_SQLAnywhere:
- Copy the code in u_dw's DBError event into your u_dw's DBError event. Then specify to Override the ancestor script. Don't forget to do that.
Note: It is recommended to simply copy this event into your u_dw so that anything that you may have enhanced in your u_dw is not inadvertently wiped out.
////////////////////////////////////////////////////////////////////////////// // // NOTE: Ancestor is overridden!! // // NOTE: This script is overridden in order to handle update errors during // a transaction... // // During a transaction, the messagebox is not displayed here. // Instead the database error is captured so that it may be displayed // later after a COMMIT or ROLLBACK (and the transaction then ends). // // NOTE: This script is also overridden in order to provide the additional // information even when there is no transaction in progress. // ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Event: dberror // // Description: // Display (or store) message that a database error has occurred // ////////////////////////////////////////////////////////////////////////////// // // Revision History // // Version // 5.0 Initial version // ////////////////////////////////////////////////////////////////////////////// string ls_message, ls_window_work = 'Unknown', ls_buffer_work = 'Unknown' string ls_msgparm[1] window lw_window_work // Prepare window work info this.of_GetParentWindow( lw_window_work ) if IsValid( lw_window_work ) then ls_window_work = & "in window titled ~r~n'" + lw_window_work.Title + "'~r~n" + & "(WindowObject is " + lw_window_work.ClassName() + ")" end if // Prepare buffer work info if buffer = Primary! then ls_buffer_work = "Primary (main one you see)" elseif buffer = Filter! then ls_buffer_work = "Filter (select menu option View/Filter to see it)" elseif buffer = Delete! then ls_buffer_work = "Delete (some row you were trying to delete)" end if ls_message = "A database error has occurred." + "~r~n~r~n" + & "DATABASE ERROR CODE: " + string(sqldbcode) + "~r~n~r~n" + & "DATABASE ERROR MESSAGE:~r~n" + sqlerrtext + "~r~n~r~n" + & "TRANSACTION OBJECT NAME:~r~n" + SQLCA.of_GetName() + "~r~n~r~n" + & "TRANSACTION IN PROGRESS:~r~n" + string(SQLCA.of_GetTransInProgress())+ "~r~n~r~n" + & "WHERE ERROR OCCURRED: " + "~r~n" + & "dberror event of " + "~r~n" + & "datawindow " + this.ClassName() + "~r~n" + & "(DataObject is " + this.DataObject + ")~r~n" + & "on row # " + string(row) + "~r~n" + & "in this buffer - " + ls_buffer_work + "~r~n" + & "which is on " + parent.ClassName() + "~r~n" + & + ls_window_work // Capture the error message (may or may not be displayed below based on if trans is in progress) SQLCA.of_SetError( ls_message ) if NOT SQLCA.of_GetTransInProgress() then //Transaction is *not* in Progress (safe to display error message now) SQLCA.of_DisplayError( "(same as above)" ) end if return 1 //Stop PowerBuilder from displaying its default message
- Copy the code in f_begin_end_tran_code into each of your windows which are updateable. There is code for the pfc_BeginTran event as well as corresponding code for the pfc_EndTran event. Make sure you add them both - This is very important!
Note: It is recommended to put this code in the lowest descendant level so that the ClassName( ) function will report the window object's name. But if you would rather sacrifice that information in order to not have to put it in every window, then you could opt to put it in an ancestor. You will still have the title information. If you put it in an ancestor, you will probably encompass non-updateable windows too - But theoretically, those should never run through the save process... So choose which way works best for you.
Note: It is recommended to put this code into every updateable window, even if there is only one table updated. That is because otherwise if some rows update, but one fails, then the other rows need to be rolled back. This allows the user to correct the error and the try to save again... without getting the -3 "Row changed between retrieve and update" message.
/* //The following code should be copied into the pfc_begintran event for updateable windows: //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // Begin a transaction if SQLCA.of_begin( ) <> 0 then SQLCA.of_DisplayError( "pfc_begintran event after" + "~r~n" + & "of_begin in" + "~r~n" + & "window titled ~r~n'" + this.Title + "'~r~n" + & "(WindowObject is " + this.ClassName() + ")" ) Return -1 //error end if Return 1 //success //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //The following code should also be copied into the pfc_endtran event for updateable windows: //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // End the transaction if SQLCA.of_end( ai_update_results ) <> 0 then SQLCA.of_DisplayError( "pfc_endtran event after" + "~r~n" + & "of_end in" + "~r~n" + & "window titled ~r~n'" + this.Title + "'~r~n" + & "(WindowObject is " + this.ClassName() + ")" ) Return -1 //error end if Return 1 //success //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< */
- You may also wish to enhance your error message in your Application Manager's pfc_logon event to take advantage of the new and improved error messaging...
SQLCA.of_SetUser (as_userid, as_password) if SQLCA.of_Connect() >= 0 then return 1 else SQLCA.of_CaptureError( "pfc_logon event after" + "~r~n" + & "connect" + "~r~n" + & "(Object is " + this.ClassName() + ")" ) SQLCA.of_DisplayError( "(same as above)" ) return -1 end if
- Test everything thoroughly and carefully. Use the Debug Spy Service to see what is going on. And when you are satisfied, then update your master source with the new n_tr, the new n_tr_SQLAnywhere, your modified application object, your modified u_dw, your modified updateable windows, and your modified application manager (if applicable).
.
Congratulations! You are done!
Notes
This Extension PFC 5.0.02 Error message is stored in the n_tr transaction object
(as an instance variable there)
(stored by u_dw's DBError event)
Error message is populated with a lot of information, and a lot of spies are recordedError message is stored in the pfc_w_master window
(as an instance variable there)
(stored by u_dw's DBError event)
Error message only contains basic informationDisplays the error message in the window's pfc_EndTran event, after transaction has ended [after of_end( ) function call, based on return code from it] Displays the error message in the window's pfc_DBError event. This event will execute after the pfc_EndTran event executes, if there was an error during the pfc_Update event and the window's error message instance variable is not an empty string. Since this extension does not populate the window's instance variable, it will always be an empty string in this extension and the pfc_DBError event will simply not execute.
Both store the error message, to be saved and then displayed after the transaction has been ended with a COMMIT or ROLLBACK. But this extension will also show much more detail about the error.
ll_rc = this.of_Execute( "BEGIN TRANSACTION" )
...and leave AutoCommit = TRUE all of the time. However, SQLAnywhere run via ODBC has a problem with nested transactions. SQLAnywhere itself can nest transactions, and it even works and gives good return codes, always! However the problem comes into play with the ODBC layer, not SQLAnywhere itself... So when you are running SQLAnywhere, through ODBC, it does not work. Here's what happens. When PowerBuilder's AutoCommit = TRUE, ODBC issues a COMMIT to the database, no matter what - whether a nested transaction has been started or not. So SQLAnywhere nested transactions will only work if you are connected directly to SQLAnywhere (not going through ODBC), using ISQL for instance. And that doesn't do us any good.
So I had to settle for flipping AutoCommit = FALSE to begin a transaction, and then flip AutoCommit = TRUE when the transaction ends.
I have reported this to Powersoft (WebExpress Issue # 459861), and they are working on a solution. I believe that they need a parameter for ODBC to tell it not to always COMMIT, without having to resort to setting PowerBuilder's AutoCommit = FALSE.
If/when they come up with a way to allow us to nest SQLAnywhere transactions using ODBC, it will only require a few simple changes to switch over to this superior way:
- Make sure that the new ODBC parameter is set in n_tr_SQLAnywhere's of_SetUser( ) function.
- Change n_tr_SQLAnywhere's of_Begin( ) function to call ll_rc = this.of_Execute( "BEGIN TRANSACTION" ) instead of setting AutoCommit = FALSE. (Adding in passing of return code.)
- Remove the no-longer-needed AutoCommit = TRUE from n_tr_SQLAnywhere's of_End( ) function.
- In n_tr_SQLAnywhere's of_End( ) function, change each of_Commit() to of_Execute( "COMMIT TRANSACTION" ). And change each of_Rollback() to of_Execute( "ROLLBACK TRANSACTION" ).
Further Reading on Transaction Management
Your Feedback is Valued and Appreciated
If you have any suggestions, questions or problems, please feel free to drop me a line at mailto:"Sharon Weinstrom Buntz" <sbuntz@texas.net>. Also, if you make me aware that you will be using this extension, then I will keep you abreast of any news or future developments. Looking forward to hearing from you.
Thank you! ~Sharon
| Download the source code | About the extension contributor |
| Revision History | ||
| 10/29/1997 | Initial Revision | Ver. 1.00 |
Interested in contributing material to this web site? Send an email to submit@pfcguide.com, but don't forget to check the contribution guidelines first.
For information or suggestions about this web site please contact webmaster@pfcguide.com
PFCGuide is
sponsored by Dynamic Technology Group.
The information on this site is provided for advice only and not to be considered a
contract or a liability against Dynamic
Technology Group.