| << 17.1.4- Transactional ASP Pages | Chapter17 | 17.2.0- COM+ Components >> |
Transaction Scope
The scope of a transaction refers to all the actions that lie between the beginning of the transaction and the end of the transaction:
- Any actions that are executed as part of the transaction are contained within the transaction's scope. These actions should be rolled back if the transaction is aborted at any stage.
- Any actions that are not executed as part of the transaction are said to be outside the transaction's scope.
So when we're dealing with transacted ASP pages, what is the scope of the transaction? Well, in ASP we start a transaction by setting the @TRANSACTION directive to a value such as REQUIRED, using the following line:
<%@TRANSACTION=REQUIRED%>
This line must be placed at the beginning of the ASP page, so in ASP the scope of a transaction always coincides with the beginning of an ASP page.
The transaction ends when the page ends – either naturally, when the execution reaches the last line of code in the ASP page, or forcibly, when we call a method such as Response.End.
So in short, it's usual that in ASP a transaction begins at the beginning of a page, and it finishes at the end of a page. So the next question is: "Can our transaction scope encompass more than one page?"
Transaction Scope Across Multiple ASP Pages
As we first learned in Chapter 7 , IIS 5.0 gives us two new methods in ASP 3.0 – the Server.Execute method and the Server.Transfer method – which allow us to call functionality from other ASP scripts into the current ASP script. For example, the following lines uses Server.Execute to force execution to jump from the currently-executing page to the beginning of a page called OtherPage.asp:
Server.Execute OtherPage.asp
When OtherPage.asp has finished executing, IIS will return to execute the remainder of the original page.
By contrast, if we use the following line then execution will jump from the current page to OtherPage.asp, and will not return to the original page on completion:
Server.Transfer OtherPage.asp
This has interesting consequences for our transactions. In particular, it's interesting to note what happens when we call one of these methods from an ASP page that has already started a transaction. For example, consider a page that contains the following code:
<%@TRANSACTION=Required%>
<%
... other ASP code ...
Server.Execute OtherPage.asp
... more ASP code ...
%>
Clearly this page is transactional, so there is a transaction in process already at the time the Server.Execute method is called. As soon as that happens, execution moves from this page to the page called OtherPage.asp. If we want the contents of OtherPage.asp to form part of the existing transaction (i.e. to be contained within the existing transaction's scope) then we can, but we need to be a little careful about the @TRANSACTION directive at the top of OtherPage.asp.
As you may recall from earlier in the chapter, there are four possible values that we can assign to the @TRANSACTION directive – they are Not_Supported, Supported, Required or Requires_New. We can choose any one of these values for the @TRANSACTION directive at the top of OtherPage.asp – and our choice will dictate whether the contents of OtherPage.asp are contained within the scope of the existing transaction:
|
@TRANSACTION= …
|
Behavior |
|
Not_Supported |
The contents of OtherPage.asp are outside the scope of the existing transaction |
|
Supported |
The contents of OtherPage.asp are within the scope of the existing transaction |
|
Required |
The contents of OtherPage.asp are within the scope of the existing transaction |
|
Requires_New |
COM+ will create a second transaction, nested within the existing transaction, just for the contents of OtherPage.asp |
If you look back at our definitions of these four values, you'll see that they tie in with the behaviors that we've described here. Don't worry too much about nested transactions – they are a rather advanced topic, and we won't be reaching such advanced stages in this book.
If you find all this a little confusing, let's have an example that illustrates some of the issues involved here.
Try It Out – A Transaction with Multiple-page Scope
In this example, we'll create a couple of pages the bank teller can use to create a brand new account within our banking system. The bank teller will enter the name of the customer, and a new account number (unfortunately we haven't implemented that auto-generation method for account numbers yet, but we'll use a little 'business rule' logic to check the chosen account Id against the existing IDs in the database). The teller is also required to enter the initial balance for the new account.
In the page that manages these changes, we'll start a transaction, and then attempt to write the new Account ID and account owner's name to the Account table of our database. Then we'll use the Server.Execute method to invoke a second ASP page, which writes the initial balance to the Register table of the database. Along the way, we'll think about what @TRANSACTION directives are appropriate for these two pages.
1. We'll be using the Bank database that was used in the previous example in this chapter. Therefore, you'll need to ensure that your Bank.mdf file is contained within the C:\MSSQL7\Data folder (or, if you're using SQL Server, that your Bank database is set up on the SQL Server machine).
2. We'll also be using the connection string details contained in the BankConnection.asp page (which we provided with the previous example in this chapter). So, ensure that the BankConnection.asp file is still saved into your BegASPFiles folder.
3. Now we'll create the new code for this example. The first page is called BankAddAccount.asp. Open your code editor and add the following code:
<HTML>
<HEAD>
<TITLE>Rocks Banking for Programmers</TITLE>
</HEAD>
<BODY>
<H2>Rocks Banking for Programmers</H2>
<H3>Add a New Account - Please enter the following details:</H3>
<FORM ACTION="CreateAcct.asp" METHOD="POST">
Enter a number for the new account:
<INPUT TYPE="TEXT" NAME="AccountNo"><BR>
Enter the name of the owner of the new account:
<INPUT TYPE="TEXT" NAME="AccountName"><BR>
Enter an initial balance: $<INPUT TYPE="TEXT" NAME="InitialBalance"><BR><BR>
<INPUT TYPE="SUBMIT" VALUE="Create New Account">
</FORM>
</BODY>
</HTML>
4. Save this as BankAddAccount.asp, into your BegASPFiles folder.
5. Here's the first of our two transactional pages – it's called CreateAcct.asp. So create a second new file and add the following to it:
<%@ TRANSACTION=REQUIRED %>
<!--#INCLUDE FILE="BankConnection.asp"-->
<!-- METADATA TYPE="typelib"
FILE="C:\Program Files\Common Files\System\ado\msado15.dll" -->
<HTML>
<HEAD><TITLE>Rocks Banking for Programmers</TITLE>
</HEAD>
<BODY>
<H2>Rocks Banking for Programmers</H2>
<H3>We'll try to create a new account for you, and report success or failure</H3>
<%
Dim dcBank, rsBank, strSQL, strAbortReason
Dim strAcctName
Session("strAcctNo") = Request.Form("AccountNo")
strAcctName = Request.Form("AccountName")
Session("dblInitialBalance") = Request.Form("InitialBalance")
If dblInitialBalance < 0 Then
ObjectContext.SetAbort
strAbortReason = "Can't create an account with a negative initial balance. " & _
"Please return and re-enter. "
End If
Set dcBank = Server.CreateObject("ADODB.Connection")
Set rsBank = Server.CreateObject("ADODB.Recordset")
dcBank.Open strConn
strSQL = "SELECT AccountID FROM Account WHERE
AccountID LIKE '" & _
Session("strAcctNo") & "'"
rsBank.Open strSQL, dcBank
If Not rsBank.EOF Then
ObjectContext.SetAbort
strAbortReason = "You have used the number of an existing account. "
End If
rsBank.Close
Set rsBank = Nothing
strSQL = ""
strSQL = "INSERT INTO Account (AccountID, HolderName)" & _
"VALUES ('" & Session("strAcctNo") & "', '" & strAcctName & "');"
dcBank.Execute strSQL
If dblInitialBalance >=0 Then
Server.Execute "SetBalance.asp"
End If
' Sub onTransactionCommit explains what to do when a transaction commits
Sub onTransactionCommit()
Response.Write "<HR><B>You've created a new account!</B><BR>" & _
"Account ID: <B>" & Session("strAcctNo") & "</B><BR> " & _
"Account Owner: <B>" & strAcctName & "</B><BR> " & _
"Initial Balance: <B>" &
FormatCurrency(Session("dblInitialBalance")) & _
"</B><BR>" & _
"<HR>[ <A href='BankAddAccount.asp'>Create another new account</A> ] " & _
"</BODY>" & _
"</HTML>"
If Not dcBank Is Nothing Then
If dcBank.State = adStateOpen Then
dcBank.Close
End If
Set dcBank = Nothing
End If
End Sub
' Sub onTransactionAbort explains what to do when a transaction is aborted
Sub onTransactionAbort()
Response.Write "<HR>" & _
"Transaction Aborted<BR>" & _
"Reason: " & strAbortReason & _
"<HR>[ <A href='BankAddAccount.asp'>Create another new account</A> ] " & _
"</BODY>" & _
"</HTML>"
If Not dcBank Is Nothing Then
If dcBank.State = adStateOpen Then
dcBank.Close
End If
Set dcBank = Nothing
End If
End Sub
%>
6. Save this as CreateAcct.asp, into your BegASPFiles folder.
7. Here's the second of our transactional ASP pages. It's called SetBalance.asp. So create one more new file and add the following code to it:
<%@TRANSACTION=Supported %>
<!--#INCLUDE FILE="BankConnection.asp"-->
<%
Dim dcBank, strSQL
Set dcBank = Server.CreateObject("ADODB.Connection")
dcBank.Open strConn
strSQL = "INSERT INTO Register " & _
"(AccountID, CheckNumber, Transdate, TransType, " & _
"TransNote, Amount, PriorBalance, ResultingBalance) " & _
"VALUES ('" & Session("strAcctNo") & "', 0, '" & Date & _
"', 'D', 'Initial Deposit', "
& CDbl(Session("dblInitialBalance")) & -
", 0, " & Session("dblInitialBalance")
& ");"
dcBank.Execute strSQL
dcBank.Close
Set dcBank = Nothing
%>
8. Save this as SetBalance.asp, into your BegASPFiles folder.
9. Now fire up your browser, browse to the page BankAddAccount.asp, and add some details to the input boxes:
|
|
Then click the Create New Account button to submit the request and create the new account.
10. The resulting page will report the success (or otherwise!) of your request:
|
|
How It Works
The first page in this example, BankAddAccount.asp, should be simplicity itself by now! It just contains an HTML form, asking the user to input values for the new account ID, account name and initial balance:
<FORM ACTION="CreateAcct.asp" METHOD="POST">
Enter a number for the new account:
<INPUT TYPE="TEXT" NAME="AccountNo"><BR>
Enter the name of the owner of the new account:
<INPUT TYPE="TEXT" NAME="AccountName"><BR>
Enter an initial balance: $<INPUT TYPE="TEXT" NAME="InitialBalance"><BR><BR>
<INPUT TYPE="SUBMIT" VALUE="Create New Account">
</FORM>
As we've said before, the account ID needs to be unique, and usually we would write a small function to automatically generate the account IDs for this application. But in this case, we'll allow the cashier to choose the account ID. Later, as part of the transaction, we'll check the 'candidate' account ID against the other IDs in the database – if the chosen account ID is already being used by an existing account then we'll 'abort' the transaction.
Now, we have two pages that perform the job of creating the account:
- The first is CreateAcct.asp, which is called directly by the HTML form above. This performs a couple of validation checks, and then adds the account ID and account name to the Account table of the database
- The second is SetBalance.asp, which is executed via a Server.Execute call from CreateAcct.asp. Its job is to add the opening balance details to the Register table of the database.
We've got a few validity checks to perform here, and we're updating both the Account and Register tables, so it would be ideal to put all these things into a transaction. Therefore, we set the following at the top of CreateAcct.asp:
<%@ TRANSACTION=Required %>
This tells COM+ that the page must run within a transaction – since there was no transaction running previously, a new transaction will start at this point.
As we've said, we don't only want the contents of CreateAcct.asp to be contained in the transaction scope – we also want the contents of the SetBalance.asp file to be contained within the scope of the same transaction. In order for this to work correctly, which value should we choose for the @TRANSACTION directive for SetBalance.asp?
If you look back to the previous pages, you'll see that we've actually used the following for the first line of SetBalance.asp:
<%@TRANSACTION=Supported %>
This means that this ASP page will take part in any existing transaction, but will not start a new one. That's fine for us – it means that the SetBalance.asp file will take part in the transaction that was started at the beginning of CreateAcct.asp.
It's important to get this right. For example, if we had
chosen @TRANSACTION=
Not_Supported,
then the contents of SetBalance.asp would still
execute when called, but they would exist outside
the scope of our transaction. So, if there was something wrong with the
submitted account ID and the transaction was aborted, SetBalance.asp
would still execute – it would not be rolled back – resulting in us attempting
to set an initial balance for a non-existant account.
OK, now we know the best choices for the @TRANSACTION directives of these two pages, let's run through what else this example does.
In CreateAcct.asp, we capture the values submitted in the form, and save them as local variables:
strAcctNo = Request.Form("AccountNo")
strAcctName = Request.Form("AccountName")
dblInitialBalance = Request.Form("InitialBalance")
Then we perform a couple of validity checks. First, we check that the bank teller has entered a, initial account balance that is greater than or equal to zero. If not, we abort the transaction and create a strAbortReason string, containing the reason for the failed transaction.
If dblInitialBalance < 0 Then
ObjectContext.SetAbort
strAbortReason = "Can't create an account with a negative initial balance. " & _
"Please return and re-enter."
End If
The second validity check is the one we mentioned before – to confirm that the requested account ID is not one that already exists in the database:
Set dcBank = Server.CreateObject("ADODB.Connection")
Set rsBank = Server.CreateObject("ADODB.Recordset")
dcBank.Open strConn
strSQL = "SELECT AccountID FROM Account WHERE
AccountID LIKE '" & _
Session("strAcctNo") & "'"
rsBank.Open strSQL, dcBank
If Not rsBank.EOF Then
ObjectContext.SetAbort
strAbortReason = "You have used the number of an existing account. "
End If
rsBank.Close
Set rsBank = Nothing
To do this, we Open a recordset using a SQL command that selects any account whose AccountID field is the same as the 'candidate' account ID. If EOF is True, then the recordset is empty and that's enough to prove that the bank teller has submitted a valid (unique) account ID. Otherwise, we abort the transaction.
At this point, we're ready to insert a new record into the Account table, containing the new account ID and name. For this, we just create a SQL INSERT command and execute it using the Connection object's Execute method:
strSQL = ""
strSQL = "INSERT INTO Account (AccountID, HolderName)" & _
"VALUES ('" & Session("strAcctNo") & "', '" & strAcctName & "');"
dcBank.Execute strSQL
Now we can use the Server.Execute method to execute the contents of SetBalance.asp page, which takes care of setting the initial balance for the new account:
If dblInitialBalance >=0 Then
Server.Execute "SetBalance.asp"
End If
As we've already discussed, SetBalance.asp has the @TRANSACTION=Supported directive, so that the entire page is contained within the scope of the existing transaction. This means that, if the transaction is aborted for any reason, then the changes made by SetBalance.asp will be rolled back along with the other state changes in this transaction.
The contents of the page are very simple. We use an ADO Connection object to open a connection to the database (we can't use the existing Connection object from CreateAcct.asp, because the object's scope does not span across the pages). Then we write a SQL INSERT command that inserts a new record into the Register table, containing the details of the initial balance of the account, and we execute it using the Connection object's Execute method:
<%
Dim dcBank, strSQL
Set dcBank = Server.CreateObject("ADODB.Connection")
dcBank.Open strConn
strSQL = "INSERT INTO Register " & _
"(AccountID, CheckNumber, Transdate, TransType, " & _
"TransNote, Amount, PriorBalance, ResultingBalance) " & _
"VALUES ('" & Session("strAcctNo") & "', 0, '" & Date & _
"', 'D', 'Initial Deposit', "
& CDbl(Session("dblInitialBalance")) & _
", 0, " & _
Session("dblInitialBalance") & ");"
dcBank.Execute strSQL
dcBank.Close
Set dcBank = Nothing
%>
The SQL command looks fairly illegible here; but once you work out the ASP, string concatenation and line continuation characters it might look something like this:
"INSERT INTO Register (AccountID, CheckNumber, Transdate, TransType, TransNote,
Amount, PriorBalance, ResultingBalance)
VALUES ('1234-1234', 0, '09/30/1999', 'D', 'Initial Deposit', 325.00, 0, 325.00);"
Now, back in CreateAcct.asp all that remains is to write the onTransactionComplete() and onTransactionAbort() routines. They are very similar to the event handlers that we saw in the previous example, so we will not cover them again here. Suffice it to say that, in this example, we use them simply to report success or failure to the user and to tidy up any remaining objects.
| << 17.1.4- Transactional ASP Pages | Chapter17 | 17.2.0- COM+ Components >> |

RSS


