For example, in the previous scenario, if Dave in dispatch had only updated the route code column that he changed the value of, Alice's changes to the address columns would not have been
Trang 1value of the column and not the Original value:
Dim objInsertCommand As New OleDbCommand("BookAuthorInsert", objConnect)
Trang 2Creating the DeleteCommand Parameters
The third and final stored procedure is used to delete rows from the source table It requires three parameters that specify the Original row values, and the code to create them is very similar to that we've just been using with the other Command objects:
Dim objDeleteCommand As New OleDbCommand("BookAuthorDelete", objConnect)
Trang 3Displaying the Command Properties
Now that the three new Command objects are ready, we display the CommandText and the parameters for each one in the page Notice that we can iterate through the Parameters collection with a For Each construct to get the values:
Dim strSQL As String 'create a new string to store vales
'get stored procedure name and source column names for each parameter
strSQL = objDataAdapter.UpdateCommand.CommandText
For Each objParam In objDataAdapter.UpdateCommand.Parameters
strSQL += " @" & objParam.SourceColumn & ","
Next
strSQL = Left(strSQL, Len(strSQL) -1) 'remove trailing comma
Trang 4outUpdate.InnerText = strSQL 'and display it
'repeat the process for the Insert command
'repeat the process for the Delete command
Executing the Update
Then we simply call the Update method of the DataAdapter to push our changes into the database via the stored procedures in exactly the same way as we did in previous examples As in earlier examples, this page uses a transaction
to make it repeatable, so the code is a little more complex than is actually required simply to push those changes into the database Basically, all we need is:
objDataAdapter.Update(objDataSet, "Books")
The code to create the transaction is the same as we used in the previous example, and you can use the [view source] link
at the bottom of the page to see it To prove that the updates do actually get carried out, you can also change the code
so that the transaction is committed, or remove the transaction code altogether
Using the NOCOUNT Statement in Stored Procedures
One point to be aware of when using stored procedures with the Update method is that the DataAdapter decides whether the update succeeded or failed based on the number of rows that are actually changed by the SQL statement(s) within the stored procedure
When a SQL INSERT, UPDATE, or DELETE statement is executed (directly or inside a stored procedure) the database returns the number of rows that were affected If there are several SQL statements within a stored procedure, it adds up the number of affected rows for all the statements and returns this value
If the returned value for the number of rows affected is zero, the DataAdapter will assume that the process (INSERT, UPDATE, or DELETE) failed However, if any other value (positive or negative) is returned, the DataAdapter assumes that the process was successful
In most cases this is fine and it works well, especially when we use CommandBuilder-created SQL statements rather
Trang 5than stored procedure to perform the updates But if a stored procedure executes more than one statement, it may not always produce the result we expect For example, if the stored procedure deletes child rows from one table and then deletes the parent row in a different table, the "rows affected" value will be the sum of all the deletes in both tables However, if the delete succeeds in the child table but fails in the parent table, the "rows affected" value will still be greater than zero So, in this case, the DataAdapter will still report success, when in actual fact it should report a failure
To get round this problem, we can use the NOCOUNT statement within a stored procedure When NOCOUNT is "ON", the number of rows affected is not added to the return value So, in our hypothetical example, we could use it to prevent the deletes to the child rows from being included in our "affected rows" return value:
SET NOCOUNT ON
DELETE FROM ChildTable WHERE KeyValue = @param-value
SET NOCOUNT OFF
DELETE FROM ParentTable WHERE KeyValue = @param-value
Update Events in the DataAdapter
In the previous chapter we saw how we can write event handlers for several events that occur for a row in a table when that row is updated In the examples we used, the row was held in a DataTable object within a DataSet, and the events occurred when we updated the row There is another useful series of events that we can handle, but this time they occur when we come to push the changes back into the original data store using a DataAdapter object
The DataAdapter exposes two events: the RowUpdating event occurs before an attempt is made to update the row in the data source, and the RowUpdated event occurs after the row has been updated (or after an error has been detected
- a topic we'll look at later) This means that we can monitor the updates as they take place for each row when we use the Update method of the DataAdapter
Handling the RowUpdating and RowUpdated Events
The example page Handling the DataAdapter's RowUpdating and RowUpdated Events (rowupdated-event.aspx) demonstrates how we can use these events to monitor the update process in a DataAdapter object When you open the
Trang 6page, you see the now familiar DataGrid objects containing the data before and after it has been updated by code within the page:
You can also see the SQL SELECT statement that we used to extract the data, and the three auto-generated statements that are used to perform the updates This page uses exactly the same code as the earlier DataAdapter.Updateexample to extract and edit the data, and to push the changes back into the database
However, you can see the extra features of this page if your scroll down beyond the DataGrid controls The remainder
of the page contains output that is generated by the handlers we've provided for the RowUpdating and RowUpdatedevents (not all are visible in the screenshot):
Trang 7Attaching the Event Handlers
The only difference between this and the code we used in the earlier example is the addition of two event handlers We have to attach these event handlers, which we've named OnRowUpdating and OnRowUpdated, to the DataAdapterobject's RowUpdating and RowUpdated properties In VB NET, we use the AddHandler statement for this:
'set up event handlers to react to update events
AddHandler objDataAdapter.RowUpdating, _
New OleDbRowUpdatingEventHandler(AddressOf OnRowUpdating)
AddHandler objDataAdapter.RowUpdated, _
New OleDbRowUpdatedEventHandler(AddressOf OnRowUpdated)
In C# we can do the same using:
objDataAdapter.RowUpdating += new OleDbRowUpdatingEventHandler(OnRowUpdating);
Trang 8objDataAdapter.RowUpdated += new OleDbRowUpdatedEventHandler(OnRowUpdated);
The OnRowUpdating Event Handler
When the DataAdapter comes to push the changes to a row into the data store, it first raises the RowUpdating event, which will now execute our event handler named OnRowUpdating Our code receives two parameters, a reference to the object that raised the event, and a reference to a RowUpdatingEventArgs object
As we're using the objects from the System.Data.OleDb namespace in this example, we actually get an
OleDbRowUpdatingEventArgs object If we were using the objects from the System.Data.SqlClient namespace,
we would, of course, get a reference to a SqlDbRowUpdatingEventArgs object
The RowUpdatingEventArgs object provides a series of "fields" or properties that contain useful information about the event:
StatementType A value from the StatementType enumeration indicating the type of SQL statement that will be
executed to update the data Can be Insert, Update, or Delete
Row This is a reference to the DataRow object that contains the data being used to update the data source
Status
A value from the UpdateStatus enumeration that reports the current status of the update and allows
it and subsequent updates to be cancelled Possible values are: Continue, SkipCurrentRow, SkipAllRemainingRows, and ErrorsOccurred
Command This is a reference to the Command object that will execute the update
TableMapping A reference to the DataTableMapping that will be used for the update
Our event handler collects the statement type (by querying the StatementType enumeration), and uses this value to decide where to get the row values for display If it's an Insert statement, the Current value of the ISBN column in the row will contain the new primary key for that row, and the Original value will be empty However, if it's an Update or Delete statement, the Original value will be the primary key of the original row in the database that corresponds to the row in our DataSet
So, we can extract the primary key of the row that is about to be pushed into the database and display it, along with the statement type, in our page:
Sub OnRowUpdating(objSender As Object, _
objArgs As OleDbRowUpdatingEventArgs)
'get the text description of the StatementType
Trang 9Dim strType = System.Enum.GetName(objArgs.StatementType.GetType(), _
objArgs.StatementType)
'get the value of the primary key column "ISBN"
Dim strISBNValue As String
Select Case strType
'add result to display string
gstrResult += strType & " action in RowUpdating event " _
& "for row with ISBN='" & strISBNValue & "'<br />"
End Sub
The OnRowUpdated Event Handler
After the row has been updated in the database, or when an error occurs, our OnRowUpdated event handler will be executed In this case, we get a reference to a RowUpdatedEventArgs object instead of a RowUpdatingEventArgsobject It provides two more useful fields:
Errors An Error object containing details of any error that was generated by the data provider when
Trang 10executing the update
RecordsAffected The number of rows that were changed, inserted, or deleted by execution of the SQL statement
Expect 1 on success and zero or -1 if there is an error
So, in our OnRowUpdated event handler, we can provide information about what happened after the update Again we collect the statement type, but this time we also collect all the Original and Current values from the columns in the row Of course, if it is an Insert statement there won't be any Original values, as the row has been added to the table
in the DataSet since the DataSet was originally filled Likewise, there won't be any Current values if this row has been deleted in the DataSet:
'event handler for the RowUpdated event
Sub OnRowUpdated(objSender As Object, objArgs As OleDbRowUpdatedEventArgs)
'get the text description of the StatementType
Dim strType = System.Enum.GetName(objArgs.StatementType.GetType(), _
objArgs.StatementType)
'get the value of the columns
Dim strISBNCurrent, strISBNOriginal, strTitleCurrent As String
Dim strTitleOriginal, strPubDateCurrent, strPubDateOriginal As String
Select Case strType
Case "Insert"
strISBNCurrent = objArgs.Row("ISBN", DataRowVersion.Current)
strTitleCurrent = objArgs.Row("Title", DataRowVersion.Current)
strPubDateCurrent = objArgs.Row("PublicationDate", _
DataRowVersion.Current)
Trang 11Case "Delete"
strISBNOriginal = objArgs.Row("ISBN", DataRowVersion.Original)
strTitleOriginal = objArgs.Row("Title", DataRowVersion.Original)
strPubDateOriginal = objArgs.Row("PublicationDate", _
DataRowVersion.Original)
Case "Update"
strISBNCurrent = objArgs.Row("ISBN", DataRowVersion.Current)
strTitleCurrent = objArgs.Row("Title", DataRowVersion.Current)
strPubDateCurrent = objArgs.Row("PublicationDate", _
DataRowVersion.Current)
strISBNOriginal = objArgs.Row("ISBN", DataRowVersion.Original)
strTitleOriginal = objArgs.Row("Title", DataRowVersion.Original)
strPubDateOriginal = objArgs.Row("PublicationDate", _
DataRowVersion.Original)
End Select
'add result to display string
gstrResult += strType & " action in RowUpdated event:<br />" _
& "* Original values: ISBN='" & strISBNOriginal & "' " _
Trang 12& "Title='" & strTitleOriginal & "' " _
& "PublicationDate='" & strPubDateOriginal & "'<br />" _
& "* Current values: ISBN='" & strISBNCurrent & "' " _
& "Title='" & strTitleCurrent & "' " _
& "PublicationDate='" & strPubDateCurrent & "'<br />"
This time we can also include details about the result of the update We query the RecordsAffected value to see if a row was updated (as we expect), and if not we include the error message from the Errors field:
'see if the update was successful
Dim intRows = objArgs.RecordsAffected
If intRows > 0 Then
gstrResult += "* Successfully updated " & intRows.ToString() _
& " row<p />"
Else
gstrResult += "* Failed to update row <br />" _
& objArgs.Errors.Message & "<p />"
End If
End Sub
AcceptChanges and the Update Process
Trang 13One important point to bear in mind is how the update process affects the Original and Current values of the rows in the tables in a DataSet Once the DataAdapter.Update process is complete (in other words all the updates for all the rows have been applied), the AcceptChanges method is called for those rows automatically So, after an update, the Current values in all the rows are moved to the Original values
However, during the update process (as you can see from our example), the Current and Original values are available
in both the RowUpdating and the RowUpdated events Therefore we can use these events to monitor changes and report errors (we'll see more in a later example)
The techniques we've used in this section of the chapter (and in earlier examples) work fine in circumstances where there are no concurrent updates taking place on the source data In other words, there is only ever one user reading from and writing to any particular row in the tables at any one time However, concurrency rears its ugly head in many applications and can cause all kinds of problems if you aren't prepared for it It's also the topic of the next section
Managing Concurrent Data Updates
To finish off our look at relational data handling in NET, we'll examine some of the issues that arise when we have multiple users updating our data - a problem area normally referred to as concurrency It's easy enough to see how such a problem could arise:
Alice in accounts receives a fax from a customer indicating that their address has changed She opens the customer record in her browser and starts to change the address column values
Just at this moment, Dave in dispatch (who received a copy of the fax) decides to update the customer's delivery route code He also opens the customer record in his browser and changes the routing code column value
While Dave is doing this, Alice finishes editing the address and saves the record back to the database
Shortly afterwards, Dave saves his updated record back to the database
What's happened is that Dave's record, which was opened before Alice saved her changes, contains the old address details
So when he saves it back to the database, the changes made by Alice are lost And while concurrency issues aren't solely confined to databases (they can be a problem in all kinds of multi-user environments) it is obviously something that we can't just ignore when we build data access applications
Avoiding Concurrency Errors
Various database systems and applications use different approaches to control the concurrent updates problem One solution is the use of pessimistic record locking When a user wants to update a record, they open it with pessimistic locking, preventing any other user opening the same record in update mode Other users can only open the record in 'read' mode until the first user saves their copy and releases their lock on the record
Trang 14For maximum run time efficiency, many database systems actually lock a 'page' containing several contiguous records rather than just a single one - but the principle is the same
However, in a disconnected environment (particularly one with occasionally unreliable network links such as the Internet) pessimistic locking is not really feasible If a user opens a record and then goes away, or the network connection fails, it will not be released It requires some other process to monitor record locks and take decisions about when and if the user will come back to update the record so that the lock can be released
Instead, within NET, all data access is through optimistic record locking, which allows multiple users to open the same record for updating - possibly leading to the scenario we described at the start of this section It means that we have to use some kind of code that can prevent errors occurring when we need to support concurrent updates There are a few options:
We can write stored procedures that do lock records and themselves manage the updates to prevent concurrency errors For example, we could add a column called "Locked" and set this when a user fetches a row for updating While it's set, no other user could open the row for updating, only for reading This is not, however, a favored approach in NET as it takes away the advantages of the disconnected model
We can arrange for our code to only update the actual columns that it changes the value of, minimizing the risk
of (but not guaranteeing to prevent) concurrency errors For example, in the previous scenario, if Dave in dispatch had only updated the route code column that he changed the value of, Alice's changes to the address columns would not have been lost
We can compare the existing values in the records with the values that were there when we created our disconnected copy of the record This way we can see if another user has changed any values in the database while we were working on our disconnected copy This is the preferred solution in NET, and there are built-in features that help us to implement it
A Concurrency Error Example
To illustrate how concurrency error can be detected, try the example page Catching Concurrency Errors When Updating
the Source Data ( concurrency-error.aspx) This page extracts a row from the source data table and displays the values in it Then it executes a SQL statement directly against the original table in the database to change the Titlecolumn of the row while the disconnected DataSet is holding the original version of the row You can see this in the screenshot after the first DataGrid control
Next the code changes a couple of columns in the disconnected DataSet table row, then calls the Update method of the DataAdapter to push this change back into the original source table We're using a CommandBuilder object to create the SQL statement that performs the update, and you can see this statement displayed in the page below the SELECTstatement that originally fetched the row Notice that it uses a WHERE clause that tests all the values of the row in the database against the values held in the DataSet
This means, of course, that (because the concurrent process has changed the row) the update fails and an error is
Trang 15returned What's happened is that the Update process expects a single row to be updated, and when this didn't happen
it reports an error The error message is displayed at the bottom of the page:
At this point, the developer would usually indicate to the user that this error had occurred, and give them the chance to reconcile the changes Exactly how this is done, and what options the user has, depends on the application requirements The usual process is to provide the user with the values that currently exist in the row as well as the values they entered, and allow them to specify which should be persisted into the data store
The Code for the 'Catching Concurrency Errors' Example
The only section of the code for this example that we haven't seen before is that which performs a concurrent update to the source table in the database while the DataSet is holding a disconnected copy of the rows It's simply a matter of creating a suitable SQL statement, a new Connection and Command object, and executing the SQL statement We collect the number of rows affected by the update and display this in the page along with the SQL statement:
'change one of the rows concurrently - i.e while the
'DataSet is holding a disconnected copy of the data
Dim strUpdate As String
Dim datNow As DateTime = Now()
Trang 16Dim strNow As String = datNow.ToString("dd-M-yy \a\t hh:mm:ss")
strUpdate = "UPDATE BookList SET Title = 'Book Written on " _
& strNow & "' WHERE ISBN = '1861001622'"
Dim intRowsAffected As Integer
Dim objNewConnect As New OleDbConnection(strConnect)
Dim objNewCommand As New OleDbCommand(strUpdate, objNewConnect)
outUpdate.InnerHtml = "Command object concurrently updated " _
& CStr(intRowsAffected) & " record(s)<br />" & strUpdate
Then the code changes the disconnected copy of the row in the DataSet table:
'change the same row in the table in the DataSet
objTable.Rows(0)("Title") = "Amateur Theatricals for Windows 2000"
objTable.Rows(0)("PublicationDate") = Now()
Trang 17Finally, all we have to do is execute the Update method of the DataAdapter object The error is trapped by the Try Catch construct (like that we've used in all the examples) and details are displayed in the page:
Try
'create an auto-generated command builder and set UPDATE command
Dim objCommandBuilder As New OleDbCommandBuilder(objDataAdapter)
objDataAdapter.UpdateCommand = objCommandBuilder.GetUpdateCommand()
'display the auto-generated UPDATE command statement
outUpdate.InnerText = objDataAdapter.UpdateCommand.CommandText
'now do the update - in this case we know it will fail
intRowsAffected = objDataAdapter.Update(objDataSet, "Books")
outResult.InnerHtml = "<b>* DataSet.Update</b> affected <b>" _
& CStr(intRowsAffected) & "</b> row."
Catch objError As Exception
'display error details
outError.innerHTML = "* Error updating original data.<br />" _
& objError.Message & "<br />" & objError.Source
End Try
Updating Just the Changed Columns
Trang 18In general, it is the process of modifying existing rows in a data store that is most likely to create a concurrency error The process of deleting rows is usually less error-prone, and less likely to cause data inconsistencies in your database Likewise, providing data entry programs are reasonably clever about how they create the values for unique columns, the process of inserting new rows is generally less of a problem
One of the ways that we can reduce the likelihood of a concurrency error during row modification, as we suggested right
at the start of this section of the chapter, is to push only the modified column values (the ones that have been changed
by this user or process) into the original data store, rather than blindly updating all of the columns Of course, this means that we can't use the Update method - we have to build and execute each SQL statement ourselves
An Example of Updating Individual Columns
The example page Managing Concurrent Updates to Individual Columns (concurrency-columns.aspx) demonstrates the process we've just been discussing Rather than updating all the columns in every row that has been modified, it only attempts to update the column values that have actually changed The code extracts a series of rows from the BookListtable and displays them, then changes some of the column values and displays the rows again:
At the bottom of the page, you can see that the code concurrently updates two of the rows in the source database table while the DataSet is holding a disconnected copy of the data And, scrolling further down the page, you can see that - after the concurrent updates have taken place - we attempt to push our changes in the DataSet back into the data store However, in this case, we're processing each of the modified rows individually by executing a custom SQL statement for each one This statement only updates the columns that have been changed within the table in the DataSet:
Trang 19What you should be able to see here is that the first update fails because we are attempting to change the title while the concurrent process has already changed this column (look back at the previous screenshot to see the original values of the rows) Following this, the second update succeeds because the concurrent process has not changed the original row
However, the third update also succeeds - even though the same row in the original table has been concurrently updated The concurrent process changed only the title column while our disconnected copy contains an updated value for only the publication date Hence, because our update code is clever enough to only update the changed columns, both updates can occur concurrently without the risk of inconsistencies arising
The Code for the 'Updating Individual Columns' Example
There are several things going on in this example page that we need to look at in more depth For example, we need to
be able to get at just the modified rows in the table within our DataSet so that we can iterate through these rows processing the update for each one Secondly, we need to look in more detail at how we create the values that we use in the WHERE clause of our SQL statements to compare to a DateTime column in SQL server
Marshalling the Changed Rows in a DataSet
As we saw in the previous chapter, every row in a table within a DataSet has a RowState property that indicates whether that row has changed since the table was filled, or since the last time the AcceptChanges or RejectChangesmethod was called So, to get a list of the changed rows we could iterate through the table looking at this property in each row, and extract just the ones we want into an array - or into another table
Trang 20This would allow us to take a table that contained updated, deleted, and inserted rows and extract these into separate arrays of rows - one each for changed rows, deleted rows, and updated rows We could then use the Update method of the DataAdapter with each table or array of rows in turn (as discussed earlier in this section of the chapter) - in the correct order to avoid any errors due to parent/child relationships within the source data tables
The general process of collecting together data and transferring it to another location is often referred to as marshalling
In our case, we want to marshal the changed rows from one table into another table, and the NET data access classes make it easy through the GetChanges method of the DataSet object It returns a DataSet object containing just the changed rows We can use the GetChanges method in two ways:
With no parameters, whereupon it returns a DataSet object with the default table (at index zero) filled with all the changed rows - e.g all the rows that have been modified, deleted, or inserted
With a DataRowState value as the single parameter, whereupon it returns a DataSet object with the default table (at index zero) filled with just the changed rows having that value for their RowState property - e.g just the rows that have been modified, or just the rows that have been deleted, or just the rows that have been inserted
Getting the Modified Rows into a New DataSet
The code in our page creates a variable to hold a DataSet object, and then executes the GetChanges method with the value DataRowState.Modified as the single parameter The new DataSet object is returned and assigned to our variable objChangeDS, and we can display the contents in the usual way using a DataGrid control defined elsewhere
in the page:
'declare a variable to hold another DataSet object
Dim objChangeDS As DataSet
'get *changed* records into the new DataSet
'copy only rows with a RowState property of "Modified"
objChangeDS = objDataSet.GetChanges(DataRowState.Modified)
'display the modified records from the table in the new DataSet
dgrResult3.DataSource = objChangeDS.Tables(0).DefaultView