Updating Data from a DataSet Object
In previous chapters you''ve regularly used a
DataSet object to store data extracted from a database, or to hold data that you''ve created dynamically using code. You also looked at the ways to edit and modify the data that the
DataSet contains. This section looks in detail at how to get those changes back into a data source such as a relational database.
ADO.NET includes the
DataAdapter object, which is used to provide the connection between a data store and a disconnected
DataSet . This object was seen in action in Chapter 8, but only so far as collecting rows from a database and pushing them into a
DataSet . To understand how the update process works for a
DataSet , we need to examine the
DataAdapter object in more depth.
Inside the DataAdapter Object
In order to understand and take advantage of many of the features of the .NET disconnected data model, especially when we discuss at concurrent data update issues later in this chapter, you must be comfortable with what''s going on behind the scenes when you use the
DataSet and
DataAdapter objects to push changes made to the data back into a data source.
The full chain of objects that are required to pull data from a data store into a
DataSet , and push the changes back into the data store after updating is shown in Figure 10-9. You can see the four main objects involved in the process–the
Connection ,
Command ,
DataAdapter , and
DataSet :

Figure 10-9:
The DataSet Object Chain
The four objects in the schematic were briefly discussed in Chapter 8. Here is a detailed look at how the whole process works:
The
Connection object defines the way that the data store will communicate with
Command objects, using a connection string and the appropriate data store provider such as SQL TDS, OLE-DB, or the ODBC driver.
The
Command object performs the task of executing the SQL statement, query, or stored procedure, etc. against the data store via the
Connection object. It contains details about that SQL statement, query or stored procedure, and the way that it should be processed.
The
DataAdapter object is the bridge between the
DataSet and the
Command objects. It specifies the organization of the tables within the
DataSet through table and column mappings, and is responsible for managing the whole process of fetching data from the data source and pushing it back to the data source.
The
DataSet is the disconnected data storage and processing unit that actually holds the data. It does so using one or more tables, and (optionally) relationships between these tables.
Notice in the schematic that there are four
Command objects involved in the process. Why? You only need one to fill a
DataSet from a data store–a suitable
SelectCommand such as a SQL
SELECT statement or the equivalent stored procedure (or table name). However, to be able to update the original data, you need the other three–
UpdateCommand ,
InsertCommand , and
DeleteCommand .
All four commands share the same
Connection object; they all have a reference to it in their
Connection property. This technique consumes far fewer resources (and hence is more efficient) than using four different ones, and works because the
DataAdapter only processes one command at a time. Connections to a data store are limited, and using the same one reduces the demands of the application considerably.
Creating the Necessary Objects
Of course, in most of the examples, you don''t explicitly create all these objects every time you want to access a data store. But that doesn''t mean they don''t exist. In fact many are automatically created in the background when required, as you perform various data access processes. Allowing the system to create them on demand also reduces the code you have to write, and can provide marginally better performance.
For example, when simply extracting data you usually create a
Connection object, a
DataAdapter object, and a
DataSet object–and then use the
Fill method of the
DataAdapter to get the data into the
DataSet :
Dim objConnect As New OleDbConnection(strConnectString)
Dim objDataAdapter As New OleDbDataAdapter(strSQLStatement, objConnect)
Dim objDataSet As New DataSet()
objDataAdapter.Fill(objDataSet, "table-name")
However, behind the scenes, when the constructor for the
DataAdapter is executed, a
Command object is created using the SQL statement and the connection object. This new
Command object is then assigned to the
SelectCommand property of the
DataAdapter object.
You can even dispense with creating a
Connection object yourself. Just pass the connection string itself into the constructor for the
DataAdapter object:
Dim objDataAdapter As New OleDbDataAdapter(strSQLStatement, strConnectString)
Again, behind the scenes, the
DataAdapter constructor is creating a new
Command object by calling its constructor with the SQL statement and (this time) the connection string. Then the
Command object constructor creates a new
Connection object using the connection string. The whole process still takes place to create the chain of four objects, even if you don''t specifically code this.
Specifying the SelectCommand
At minimum, when creating a
DataAdapter object to
Fill a
Dataset , only
the SelectCommand is required and this must always be provided. As you''ve seen, this is usually specified as a string (the SQL statement, query string, table name, or stored procedure name) in the constructor for the object. Of course, there''s nothing to stop you creating a
Command object directly and assigning this to the
SelectCommand property of the
DataAdapter :
Dim objConnect As New OleDbConnection(strConnectString)
Dim objCommand As New OleDbCommand(strSQLStatement, objConnect)
Dim objDataAdapter As New OleDbDataAdapter(objCommand)
Or, in an even more verbose way:
Dim objConnect As New OleDbConnection(strConnectString)
Dim objCommand As New OleDbCommand(strSQLStatement, objConnect)
Dim objDataAdapter As New OleDbDataAdapter()
objDataAdapter.SelectCommand = objCommand
While it''s hard to see when you could use the last of these, it could be a useful technique when you already have a
DataAdapter that you want to reuse by just changing the
SelectCommand to reference a different
Command object.
Specifying the Other Commands
To be able to fill a
DataSet , you only need a
SelectCommand –but to push the changes back to the data store, you must provide the appropriate
Command objects for the
UpdateCommand ,
DeleteCommand , and
InsertCommand properties of the
DataAdapter .
You don''t always need all three, for example if you are only changing existing rows within the data source (if the
DataSet object only contains modified rows, and no added or deleted rows), you only need to specify a suitable
Command object for the
UpdateCommand property of the
DataAdapter . The same logic applies if you are only deleting rows or inserting new rows. However, if the
DataSet contains modified, deleted, and added rows, you have to specify suitable
Command objects for all the matching
DataAdapter properties.
What is a suitable
Command object? It''s pretty obvious that this is a
Command with a connection specified to the appropriate data store (via its associated
Connection object), and which specifies a suitable SQL statement or stored procedure that will add, delete, or update the rows. We''ll show some examples later in this section in more detail. However, ADO.NET can also help out by generating suitable SQL statements automatically for us.
Command Builder Objects and Auto-generated Commands
ADO.NET tries to make your life easier when you use a
DataSet to update a data store by providing
CommandBuilder objects, such as the
SqlCommandBuilder for use with SQL TDS and the
OleDbCommandBuilder for use with an OLE-DB provider. These objects can create suitable auto-generated commands for use when pushing changes back to a data store via a
DataAdapter object.
All you have to do is create a
CommandBuilder object, specifying as the parameter to its constructor the
DataAdapter you want to use it with:
Dim objCommandBuilder As New OleDbCommandBuilder(objDataAdapter)
Then, when you call the
Update method of this
DataAdapter , it will automatically use the
CommandBuilder to create the
INSERT ,
DELETE , and
UPDATE commands for the process, and assign them to the
InsertCommand ,
DeleteCommand , and
UpdateCommand properties of the
DataAdapter . Once the
Update methods ends, the
InsertCommand ,
DeleteCommand , and
UpdateCommand properties are set back to
Nothing (
null in C#).
However, if the
InsertCommand ,
DeleteCommand , or
UpdateCommand properties are not
Nothing (
null ) when the
Update method is called, the
CommandBuilder does not replace any existing statement. This means you can assign your own custom SQL statements or stored procedure details to one or more of these properties before calling the
Update method. In this case, the specified SQL statement or stored procedure is used for that part of the
Update process, and will remain assigned to the
InsertCommand ,
DeleteCommand , or
UpdateCommand property afterwards.
Notice that you can provide a SQL statement or stored procedure for one or two of the
InsertCommand ,
DeleteCommand , and
UpdateCommand properties and allow the
CommandBuilder to automatically set the remaining ones.
The
CommandBuilder also exposes three methods that you can use if you want to retrieve the auto-generated commands for the current operation. While you probably don''t need to use these methods in your applications, they are useful for displaying the auto-generated commands in your example pages. Remember that the
CommandBuilder sets the
InsertCommand ,
DeleteCommand , and
UpdateCommand properties back to
Nothing once the
Update process completes, so you can''t access these properties to see the auto-generated commands it used.
In your example pages, you can take advantage of the
GetDeleteCommand ,
GetInsertCommand ,
GetUpdateCommand methods of the
CommandBuilder to assign the auto-generated commands to the
InsertCommand ,
DeleteCommand , and
UpdateCommand properties of the
DataAdapter before you call the
Update method. You can then access them afterwards as shown in the following code:
''create a CommandBuilder instance for the current DataAdapter
Dim objCommandBuilder As New OleDbCommandBuilder(objDataAdapter)
''set the update, insert and delete commands for the DataAdapter
objDataAdapter.DeleteCommand = objCommandBuilder.GetDeleteCommand()
objDataAdapter.InsertCommand = objCommandBuilder.GetInsertCommand()
objDataAdapter.UpdateCommand = objCommandBuilder.GetUpdateCommand()
''call the Update method
objDataAdapter.Update(objDataSet)
''read back the auto-generated commands
strDeleteCommand = objDataAdapter.DeleteCommand
strInsertCommand = objDataAdapter.InsertCommand
strUpdateCommand = objDataAdapter.UpdateCommand
The
CommandBuilder creates and returns
Command objects that specify the appropriate SQL statements for an
Update process through the
DataAdapter it is attached to. It can figure these out by looking at the
SelectCommand property of the
DataAdapter , the table structure, and table and column mappings. You''ll see what the SQL statements that these methods create look like in the next example in this chapter. In the meantime, however, you should be aware of a few limitations of the auto-generated command feature:
The rows in a table in the
DataSet must have originally come from a single table, and can be used only to update a table of the same format (generally the same source table).
The source table must have a primary key defined (it can be a multiple-column primary key), or it must have at least one column that contains unique values. This column (or columns) must be included in the rows that are returned by the
SELECT statement or query that is used for the
SelectCommand .
Table names that include special characters such as spaces, periods, quotation marks, or other non-alphanumeric characters cannot be used (even if delimited by square brackets). However, fully qualified table names that do include the period character (such as
dbo.BookList ) can be used.
Of course, you can create your own command strings if required, rather than using the auto-generated commands provided by the
CommandBuilder , and have the
DataAdapter use these instead of the auto-generated ones. In later examples, you''ll see how this is useful when working with stored procedures that perform the updates to the data store, and with custom SQL statements.
The DeriveParameters Method
One other useful feature that the
CommandBuilder provides is the ability to automatically create appropriate
Parameter objects. This includes both the situation when we are using stored procedures to update the data source, as well as when you are using them to extract data from a data store.
The
DeriveParameters method of the
CommandBuilder object takes as its single parameter a reference to a
Command object, and returns this
Command object with its
Parameters collection populated with the appropriate
Parameter objects. All that''s required then is to fill in the values:
''create the Connection, Command and DataAdapter
Dim objConnect As New OleDbConnection(ConnectionString)
Dim objCommand As New OleDbCommand(SQLStatement, objConnect)
Dim objDataAdapter As New OleDbDataAdapter(objCommand)
''create a CommandBuilder for this DataAdapter
Dim objCommandBuilder As New OleDbCommandBuilder(objDataAdapter)
''derive the parameters and set their values
objCommandBuilder.DeriveParameters(objCommand)
objCommand.Parameters("param1-name").Value = thevalue1
objCommand.Parameters("param2-name").Value = thevalue2
...etc...
However, be aware that the
DeriveParameters method requires an extra call to the data store to get information about the parameters, and so is generally inefficient. You might use it during development to find out what parameters are required (you can iterate through the
Parameters collection examining them after calling
DeriveParameters ), but you should avoid using it in release code unless absolutely necessary.
Using the DataAdapter.Update Method
The example page Updating Data with a DataAdapter and DataSet Object (
update-with-dataset.aspx ) demonstrates the simplest way to use a
DataAdapter object to update the source data with changes made to the rows stored in a
DataSet object. This example simply reads in a rowset from the
BookList table in your
WroxBooks sample database, changes some of the rows, then pushes the changes back into the data store. As shown in Figure 10-10, the code in the page deletes or removes four rows from the original table, modifies values in three other rows, and adds a new row. You can see this by comparing the contents of the table in the two
DataGrid controls on the page:

Figure 10-10:
As you can see from the note at the bottom of the page, the code uses a connection-based transaction to prevent the changes being permanently applied to the source data. If they were, the example page would fail to work the next time, as some of the rows would have been deleted and primary key violations would occur due to the new row already being present in the source table. However, you can change the code to commit the transaction to verify that it actually works and does update the original data.
You can also see the auto-generated commands that are used by the
DataAdapter to update the source data. It''s obvious that these are SQL statements, with question-mark characters as placeholders for the values used to update the table in our target data source. We''ll look at them in more detail shortly.
The Code for the ''Updating with a DataAdapter'' Example
As shown in the following code, the
SELECT statement used is simple enough–it just selects a subset of the rows in your
BookList table. Then you can use the now familiar technique to create and fill the
DataSet with your source data. This is covered in detail in previous chapters, so we''re simply listing the code here:
strSelect = "SELECT * FROM BookList WHERE ISBN LIKE ''07645437%'' " _
& "OR ISBN LIKE ''07645438%''"
Dim objDataSet As New DataSet()
Dim objConnect As New OleDbConnection(strConnect)
Dim objDataAdapter As New OleDbDataAdapter(strSelect, objConnect)
Try
objDataAdapter.Fill(objDataSet, "Books")
Catch objError As Exception
outError.innerHTML = "* Error while accessing data.<br />" _
& objError.Message & "<br />" & objError.Source
Exit Sub
End Try
In your example, you want to be able to see which rows have been changed, and the
Update method also depends on this information to be able to correctly update the original data in your database. One way to fix the current state of all the rows in all the tables in a
DataSet (as seen in the previous chapter) is to call the
AcceptChanges method to accept all the changes that have been made to the
DataSet .
In fact, in your example it''s not strictly necessary because the
Fill method automatically sets the status of all the rows to
Unchanged . However (as shown in the following code) it illustrates the process, and would be necessary if you had made any changes since you originally filled the
DataSet that you don''t want to flush back into the database. In later examples, we''ll take advantage of this.
You''ll also need to refer to the
Books table in your
DataSet in several places within your code, so create this reference next. Then you can display the contents of the
Books table that is currently held in your
DataSet . Simply bind the default view of the table to a
DataGrid control named
dgrResult1 that is declared elsewhere in the HTML section of the page.
''accept the changes to "fix" the current state of the DataSet contents
objDataSet.AcceptChanges()
''declare a variable to reference the Books table
Dim objTable As DataTable = objDataSet.Tables("Books")
''display the contents of the Books table before changing data
dgrResult1.DataSource = objTable.DefaultView
dgrResult1.DataBind() ''and bind (display) the data
Changing the Rows in the DataSet
Now you''re ready to make some changes to the data. The following code shows that you can use exactly the same technique as in the previous chapter examples. After making these changes to the
Books table in your
DataSet display the contents again. Notice that you have to use a date string that is in the correct format for the column in your table. In the example where the value of a parameter object is set, use the format "
yyyy-mm-dd " as this is a suitable format for the SQL
DateTime field. Here you''re using the format "
mm-dd-yyyy " as this is the format of the ADO.NET table column.
''now change some records in the Books table
objTable.Rows(0).Delete()
objTable.Rows(1)("Title") = "Amateur Theatricals for Windows 2000"
objTable.Rows(2).Delete()
objTable.Rows(3).Delete()
objTable.Rows(4)("PublicationDate") = "01-01-2002" ''see note below
objTable.Rows.Remove(5)
''notice that using the Remove method on row 5 (rather than marking
''it as deleted) means that the next row then becomes row 5
objTable.Rows(5)("ISBN") = "200000000"
''add a new row using an array of values
Dim objValsArray(2) As Object
objValsArray(0) = "200000001"
objValsArray(1) = "Impressionist Guide to Painting Computers"
objValsArray(2) = "05-02-2002" ''see note below
objTable.Rows.Add(objValsArray)
''display the contents of the Books table after changing the data
dgrResult2.DataSource = objTable.DefaultView
dgrResult2.DataBind() ''and bind (display) the data
Creating the Auto-Generated Commands
OK, so now you can update your data source. The first step in this part of the process is to create the commands that the
DataAdapter will use to push the changes into the database. You can use a
CommandBuilder to create the three
Command objects it requires, and assign these to the appropriate properties of the
DataAdapter so that you can retrieve and display them afterwards.
''create command builder commands to update insert and delete rows
Dim objCommandBuilder As New OleDbCommandBuilder(objDataAdapter)
''set the update, insert and delete commands for the DataAdapter
''this is only required because we want to access them afterwards
''if omitted, commands are set to null when update completes
objDataAdapter.DeleteCommand = objCommandBuilder.GetDeleteCommand()
objDataAdapter.InsertCommand = objCommandBuilder.GetInsertCommand()
objDataAdapter.UpdateCommand = objCommandBuilder.GetUpdateCommand()
Pushing the Changes Back into the Data Source
As your example uses a transaction (so that you can re-run the page) you have to explicitly open the connection to the database. If you weren''t using a transaction, you could remove the
Open method call as well (the
DataAdapter automatically opens the connection when you call the
Update method, then closes it afterwards). Then, as shown in the following code, make a call to the
BeginTransaction method of the connection.
Next (only because you''re using a transaction in your example) you have to explicitly enroll all the
Command objects into the transaction. Then you can call the
Update method of the
DataAdapter to push all the changes you''ve made to the rows in the
DataSet back into the data source automatically. Notice that the name of the table that contains the changes we want to push back into the data source is specified.
Normally that''s all you would need to do. However, you are performing the update within a transaction so that you can roll it back again afterwards – allowing you to run the same page again without getting the errors that would occur from inserting and deleting the same rows again. So finish off by rolling back this transaction.
''start a transaction so we can roll back changes if required
objConnect.Open()
objConnect.BeginTransaction()
''attach the current transaction to all the Command objects
''must be done after setting Connection property
objDataAdapter.DeleteCommand.Transaction = objTransaction
objDataAdapter.InsertCommand.Transaction = objTransaction
objDataAdapter.UpdateCommand.Transaction = objTransaction
''perform the update on the original data
objDataAdapter.Update(objDataSet, "Books")
objTransaction.Rollback()
Viewing the Auto-Generated Commands
The example page displays the auto-generated commands that were created by the
CommandBuilder object so that you can see what they look like. The code is placed at the end of the page, extracting the command strings and placing them in
<div> elments located within the HTML section of the page.
''display the SQL statements that the DataSet used
''these are created by the CommandBuilder object
outInsert.InnerText = objDataAdapter.InsertCommand.CommandText
outDelete.InnerText = objDataAdapter.DeleteCommand.CommandText
outUpdate.InnerText = objDataAdapter.UpdateCommand.CommandText
If you examine these command strings (shown again in Figure 10-11), you can see that they are outline or pseudo SQL statements, containing question-mark placeholders where the values from each row are inserted when the statements are executed. Notice how they only perform the action on the source table if the row has not been changed by another process in the meantime (that is, while the
DataSet was holding the rows). The
DataSet is a disconnected data repository, and so the original rows could have been updated, existing rows deleted, or new rows added with the same primary key by another user or process.

Figure 10-11:
Later in this chapter you''ll be looking in detail at how ADO.NET manages concurrent updates to a data store, and how you can manage them yourself. In the meantime, there are a few other issues that you need to look at when using the
Update method of the
DataAdapter object.
Checking How Many Rows Were Updated
The
Update method returns the number of rows that were updated in the source table. While you didn''t take advantage of this in your examples, it''s pretty easy to do. Simply declare an
Integer variable and assign the result of the
Update method to it:
Dim intRowsUpdated As Integer
intRowsUpdated = objDataAdapter.Update(objDataSet, "table-name")
Specifying the Tables When Updating Data
As you''ve seen, the
DataAdapter object''s
Update method provides a really easy and efficient way to update the source data. If you have more than one table in the
DataSet , simply call the method once for each table to automatically update the source data with all the changes to rows in that table. The changes are applied in the order that the rows exist within the table in the
DataSet .
There is one point to watch out for, however. If the source data tables contain foreign keys, in other words there are enforceable relationships between the tables then the order that the tables are processed can cause errors to occur. It all depends on the type of updates you''re carrying out, and the rules or triggers you have inside the source database.
For example, if your
DataSet contained rows that originally came from the
BookList ,
AuthorList , and
BookPrices tables, you could add a new book to the
Books table in the
DataSet and add matching rows (based in the ISBN that acts as the primary and foreign keys) to the
Authors and
Prices tables in the
DataSet .
When you execute the
Update method, however, it will only work if the
Books table is the first one to be processed. If you try to process the
Authors or
Prices table first, the database will report an error because there will be no parent row with an ISBN value to match the newly inserted child rows. You are trying to insert orphan rows into the database table, and thus breaking referential integrity rules.
In other words, to insert a new book in our example, you would have to use:
objDataAdapter.Update(objDataSet, "Books")
objDataAdapter.Update(objDataSet, "Authors")
objDataAdapter.Update(objDataSet, "Prices")
However, if you have deleted a book and all its child rows from the
Authors and
Prices tables in the
DataSet , the opposite applies. You can''t delete the parent row while there are child rows in the database table, unless the database contains rules or triggers that cascade the deletes to remove the child rows.
And if it does, the delete operations carried out for the child tables would fail, because the rows would have already been deleted. This means that you probably want to process the
Books table in your
DataSet last rather than first:
objDataAdapter.Update(objDataSet, "Authors")
objDataAdapter.Update(objDataSet, "Prices")
objDataAdapter.Update(objDataSet, "Books")
But if you have carried out both insert and delete operations on the tables, neither method will work correctly. In this case, you need to process the updates in a more strictly controlled order. Let''s look at what this involves when we examine concurrency issues later on in this chapter (in the section Marshalling the Changed Rows in a DataSet). First, we''ll briefly examine some of the other ways that you can use the
Update method.
Automatically Updating the Default Table in a DataSet
If you have created a table mapping in the
DataSet for the default table, you can execute the
Update method without specifying the table name. We discussed how to create table mappings in the previous chapter. Basically, create a variable to hold a
TableMapping object and then call the
Add method of the
DataAdapter object''s
TableMappings collection to create the new table mapping. Specify the string "
Table " to indicate that you are creating a default table mapping, and the name of the table:
Dim objTableMapping As DataTableMapping
objTableMapping = objDataAdapter.TableMappings.Add("Table", "DefaultBookList")
Now you can call the
Update method without specifying the name of the table:
objDataAdapter.Update(objDataSet)
An error occurs if this mapping does not exist when the
Update method is called without specifying the name of a table.
Updating Subsets of Rows from a Table
The
DataAdapter object''s
Update method can also be used to push changes from a collection or array of
DataRow objects into the data source. All the rows must come from the same source table, and there must be a default table mapping set up as described in the previous section. The updates are then processed in the order that they exist in the array.
To create an array of
DataRow objects you can use the
All property of a table''s
Rows collection:
Dim arrRows() As DataRow
arrRows = objDataSet.Tables(0).Rows.All
Then you can push the changes in this array of rows into the data source using the
Update method and specifying this array:
objDataAdapter.Update(arrRows)
This technique is useful if you have an array of rows that contain our changed records, rather than one that contains all the rows in the original table.
Updating from a DataSet Using Stored Procedures
Near the start of the chapter, we showed you how to use stored procedures within a database to update the source data. In that example, you used a
Command object to execute the stored procedures. Meanwhile, the previous example showed how to use the auto-generated commands with a
DataSet to update data automatically.
Of course, you don''t have to use auto-generated commands with a
DataSet . Instead you can use your own custom SQL statements or stored procedures to do the same thing. Just create the appropriate
Command objects for the
InsertCommand ,
DeleteCommand ,
and UpdateCommand properties of the
DataAdapter , and call the
Update method as before. Then your custom SQL statements or stored procedures are used to push the changes back into the data store.
The previous example also updated only a single table (a pre-requisite when using the auto-generated commands). However, often you have a more complex task to accomplish when updating the source data. For example, the rows in the table in your
DataSet might have originally been created from several source tables, perhaps by using a
JOIN statement in the SQL query or some complex stored procedure. This was demonstrated at the beginning of the previous chapter, where you had a table containing data drawn from both the
BookList and the
BookAuthors tables in your sample database. When you come to push changes to data like this back into your database, you need to use some process that can disentangle the values in each row and perform a series of staged updates to the original tables, thereby maintaining integrity within the database.
The example page Updating Complex Data with a DataSet and Stored Procedures (
complex -
dataset -
update.aspx ) shown in Figure 10-12 demonstrates all of these techniques and features.

Figure 10-12:
It extracts some data from the sample database using a stored procedure that joins two tables, and displays it in a
DataGrid . Then it changes some of the rows in the original table and displays the data again. Finally, it pushes the changes back into the data source using stored procedures that we''ve provided within the database.
At the top of the page you can see the values of the four
Command objects''
CommandText properties. The
SelectCommand is a stored procedure named
GetBookprices that takes a single parameter (the ISBN) – which we provide inline. This stored procedure joins the
BookList and
BookPrices tables, and returns a rowset containing values from both tables.
CREATE PROCEDURE GetBookPrices
@ISBN varchar(10) AS
SELECT BookList.ISBN, BookList.Title, BookPrices.Currency, BookPrices.Price
FROM BookList JOIN BookPrices ON BookList.ISBN = BookPrices.ISBN
WHERE BookList.ISBN LIKE @ISBN
The other three commands shown in Figure 10-12 are obviously not auto-generated SQL statements, and they don''t contain the question-mark placeholders. They are of course the names of three stored procedures within the sample database, and the names of the parameters are added to the display as well–these are not actually part of the command strings.
At the bottom of the page is a note about the transaction that is used to prevent the updates being permanently committed to the data store so that you can re-run the page (without this the updates to the source data would prevent the page from working next time).
The Update Stored Procedures for the Example Page
Your
DataSet table holds rows that are created from two different tables in the database, and so the auto-generated commands from a
CommandBuilder cannot be used to persist inserts, deletes, or updates that are made to rows in the table in the
DataSet . Instead you can use three stored procedures.
The
BookPriceUpdate stored procedure takes as parameters the ISBN of the book (which is the primary key in the
BookList table and part of the primary key in the
BookPrices table), the name of the currency in the
BookPrices table (which is the other half of the primary key in this table), and the actual value for the
Price column in the
BookPrices table. It uses these values to update the matching row in the
BookPrices table:
CREATE PROCEDURE BookPriceUpdate
@ISBN varchar(10),
@Currency varchar(3),
@Price money
AS
UPDATE BookPrices SET Price=@Price WHERE Currency=@Currency AND ISBN = @ISBN
The
BookPriceInsert stored procedure takes as parameters the ISBN, title, currency, and price values that it will use to insert a new row into the
BookList table and a new row into the
BookPrices table. Note that, as shown in the following code, it first checks to see if a book with the specified ISBN already exists in the
BookList table (as it might if we are only inserting a price in a different currency). In this case, it just inserts the new
BookPrices row.
CREATE PROCEDURE BookPriceInsert
@ISBN varchar(10),
@Title varchar(100),
@Currency varchar(3),
@Price money
AS
SELECT ISBN FROM BookList WHERE ISBN = @ISBN
IF @@ROWCOUNT = 0
INSERT INTO BookList(ISBN, Title) VALUES (@ISBN, @Title)
INSERT INTO BookPrices(ISBN, Currency, Price) VALUES (@ISBN, @Currency,@Price)
Finally, the
BookPriceDelete stored procedure takes only two parameters–the ISBN of the book and the name of the currency for the row it will delete in the
BookPrices table (see the following code). However, if there are no price rows left for this book after deleting the specified one, it also deletes the matching row from
BookList table. OK, so it''s a pretty contrived example, but it demonstrates the way that you can use stored procedures to manipulate multiple tables from the
Update method of the
DataAdapter .
CREATE PROCEDURE BookPriceDelete
@ISBN varchar(10),
@Currency varchar(3)
AS
DELETE FROM BookPrices
WHERE ISBN = @ISBN AND Currency = @Currency
SELECT ISBN FROM BookPrices WHERE ISBN=@ISBN
IF @@ROWCOUNT = 0
DELETE FROM BookList WHERE ISBN=@ISBN
The Code for the ''Updating with Stored Procedures'' Example
So, all you need to do now is use these three stored procedures as the command text for the
Command objects in the
DataAdapter object''s
UpdateCommand ,
InsertCommand , and
DeleteCommand properties. The first part of the code in the page simply fills the
DataSet from the database using the same techniques as discussed in earlier examples and earlier chapters, so we aren''t repeating that here.
Next, the code changes some of the values in the rows in the
DataSet , deleting the first row, updating the price in the second row, and adding a new row:
''declare a variable to reference the Books table
Dim objTable As DataTable = objDataSet.Tables("Books")
''change some rows in the DataSet table
''delete the first row
objTable.Rows(0).Delete()
''update price in the second row
objTable.Rows(1)("Price") = 299.99
''add a new row using an array of values
Dim objValsArray(3) As Object
objValsArray(0) = "200000001"
objValsArray(1) = "Impressionist Guide to Painting Computers"
objValsArray(2) = "USD"
objValsArray(3) = "29.99"
objTable.Rows.Add(objValsArray)
Using Dynamic Parameters with a Stored Procedure
The important point to note in this example is that you''re specifying which columns will provide the values for the parameters when the
Command is executed, rather than specifying actual values for the parameters. You are creating a dynamic parameters that are the equivalent to the question-mark placeholders you saw in the SQL statements for the update, delete, and insert command in the previous example. The appropriate one of these commands will be executed for each row in the
DataSet table that has been modified (has a
RowState property value of
DataRowState.Modified ), deleted (has a
RowState property value of
DataRowState.Deleted ), or inserted (has a
RowState property value of
DataRowState.Added ).
To specify a dynamic parameter, set the
SourceColumn property of the
Parameter object to the name of the column from which the value for the parameter will come. However, you''ll recall that each column can expose four different values (the
DataRowVersion ):
Original ,
Current ,
Default , and
Proposed . You can specify which of these values you want the parameter to use by setting the
SourceVersion property of the
Parameter object as well.
This means you can specify the
Original value of the column as the parameter value (useful if it is being used to look up or match a value with the original value of that column in the source table), or the
Current value of the column if you are updating that column in the table. In other words, you would specify that the parameter should use the
Original value of this column from each row when it''s part of the SQL
WHERE clause (and so should match the existing value in the database tables) or the
Current value when it''s part of the
SET clause.
The UpdateCommand and the Dynamic Parameters
So let''s get on and build the necessary
Command objects. Let''s start with the one for the
UpdateCommand . Create a new
Command object and specify that the
CommandType is a stored procedure. Then you can create the parameters that to be used with this
Command object.
The first parameter is used to match the ISBN code, and so it uses the
Original value of that column. The code is similar for the remaining two parameters (
Currency and
Price ). However, while the
Currency parameter also uses the
Original value of the column, the
Price must use the
Current version of the data for this column in the rows, because this value will be used to update the original rows in the database table. It will become part of the
SET clause in the SQL statement that is executed by the stored procedure.
Then, once all the parameters are ready, you can specify that this
Command object be used as the update command by assigning it to the
DataAdapter object''s
UpdateCommand property.
'' create the UpdateCommand and parameters
Dim objUpdateCommand As New OleDbCommand("BookPriceUpdate", objConnect)
objUpdateCommand.CommandType = CommandType.StoredProcedure
''now create the Parameter objects and add to the Command object
Dim objParam As OleDbParameter
objParam = objUpdateCommand.Parameters.Add("ISBN", OleDbType.VarChar, 10)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "ISBN"
objParam.SourceVersion = DataRowVersion.Original ''used in SQL WHERE clause
objParam = objUpdateCommand.Parameters.Add("Currency", OleDbType.VarChar, 3)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "Currency"
objParam.SourceVersion = DataRowVersion.Original ''used in SQL WHERE clause
objParam = objUpdateCommand.Parameters.Add("Price", OleDbType.Double)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "Price"
objParam.SourceVersion = DataRowVersion.Current ''used in SQL SET clause
''now specify this Command object as the UpdateCommand
objDataAdapter.UpdateCommand = objUpdateCommand
The InsertCommand and the Dynamic Parameters
The
InsertCommand requires four parameters, as shown in the following code. Note that in this case the stored procedure uses the ISBN value in the
SET clause of the SQL statement rather than the
WHERE clause, to set the value of the newly inserted rows, so it must use the
Current value of the column and not the
Original value.
The remaining three parameters are the
Title that is placed, along with the ISBN, into the new row in the
BookList table; and the currency and price to be instered, along with the ISBN, into the
BookPrices table. Finally you can specify that this
Command object is the insert command by assigning it to the
DataAdapter object''s
InsertCommand property.
Dim objInsertCommand As New OleDbCommand("BookPriceInsert", objConnect)
objInsertCommand.CommandType = CommandType.StoredProcedure
objParam = objInsertCommand.Parameters.Add("ISBN", OleDbType.VarChar, 10)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "ISBN"
objParam.SourceVersion = DataRowVersion.Current ''used in SQL SET clause
objParam = objInsertCommand.Parameters.Add("Title", OleDbType.VarChar, 100)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "Title"
objParam.SourceVersion = DataRowVersion.Current ''used in SQL SET clause
objParam = objInsertCommand.Parameters.Add("Currency", OleDbType.VarChar, 3)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "Currency"
objParam.SourceVersion = DataRowVersion.Current ''used in SQL SET clause
objParam = objInsertCommand.Parameters.Add("Price", OleDbType.Double)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "Price"
objParam.SourceVersion = DataRowVersion.Current ''used in SQL SET clause
objDataAdapter.InsertCommand = objInsertCommand
The DeleteCommand and the Dynamic Parameters
The third and final stored procedure is used to delete rows from the source table(s). It requires just two parameters (the ISBN and the currency) and these take their values from the
Original row values.
Otherwise, the code to create them is very similar to that you''ve just been using with the other
Command objects.
Dim objDeleteCommand As New OleDbCommand("BookPriceDelete", objConnect)
objDeleteCommand.CommandType = CommandType.StoredProcedure
objParam = objDeleteCommand.Parameters.Add("ISBN", OleDbType.VarChar, 10)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "ISBN"
objParam.SourceVersion = DataRowVersion.Original ''used in SQL WHERE clause
objParam = objDeleteCommand.Parameters.Add("Currency", OleDbType.VarChar,3)
objParam.Direction = ParameterDirection.Input
objParam.SourceColumn = "Currency"
objParam.SourceVersion = DataRowVersion.Original ''used in SQL WHERE clause
objDataAdapter.DeleteCommand = objDeleteCommand
Displaying the Command Properties
Now that the three new
Command objects are ready, you can display the
CommandText and the parameters for each one in the page. Notice that you can iterate through the
Parameters collection with a
For Each construct to get the values.
''get stored procedure name and source column names for each parameter
Dim strSQL As String = objDataAdapter.UpdateCommand.CommandText
For Each objParam In objDataAdapter.UpdateCommand.Parameters
strSQL &= " @" & objParam.SourceColumn & ","
Next
strSQL = Left(strSQL, Len(strSQL) –1) ''remove trailing comma
outUpdate.InnerText = strSQL ''and display it
...
''repeat the process for the Insert command
...
''repeat the process for the Delete command
...
Executing the Update
Simply call the
Update method of the
DataAdapter to push your changes into the database via the stored procedures in exactly the same way as you 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 you need is:
objDataAdapter.Update(objDataSet, "Books")
The code to create the transaction is the same as 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 you use
CommandBuilder -created SQL statements rather than stored procedure to perform the updates. But if a stored procedure executes more than one statement, it may not always produce the result you 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 around this problem, you 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. You could use it to prevent the deletes to the child rows from being included in your ''rows affected'' 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 you saw how to write event handlers for several events that occur for a row in a table when that row is updated. In the examples, the row was held in a
DataTable object within a
DataSet , and the events occurred when the row was updated. There is another useful series of events that can be handled, but this time they occur when you 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 you can monitor the updates as they take place for each row when you use the
Update method of the
DataAdapter .
Handling the RowUpdating and RowUpdated Events
The example page Handling the DataAdapter''s RowUpdating and RowUpdated Events (
rowupda ted-event.aspx ) demonstrates how you can use these events to monitor the update process in a
DataAdapter object. When you open the page, shown in Figure 10-13, 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 is used to extract the data, and the three auto-generated statements that are used to perform the update. This page uses exactly the same code as the earlier
DataAdapter.Update example to extract and edit the data, and to push the changes back into the database. The extra features can be seen once you scroll down, as shown in Figure 10-14.

Figure 10-13:

Figure 10-14:
The remainder of the page contains three sets of output that is generated by the handlers you''ve provided for the
RowUpdating and
RowUpdated events—one each for a deleted row, an updated row, and a row added to the
DataSet table.
Attaching the Event Handlers
One difference between the code in this page, and the code used in the earlier examples, is the addition of two event handlers. Attach these event handlers, which are named
OnRowUpdating and
OnRowUpdated to the
DataAdapter object''s
RowUpdating and
RowUpdated properties. In VB.NET, you can use the
AddHandler statement for this:
AddHandler objDataAdapter.RowUpdating, _
New OleDbRowUpdatingEventHandler(AddressOf OnRowUpdating)
AddHandler objDataAdapter.RowUpdated, _
New OleDbRowUpdatedEventHandler(AddressOf OnRowUpdated)
In C# you can do the same using:
objDataAdapter.RowUpdating += new OleDbRowUpdatingEventHandler(OnRowUpdating);
objDataAdapter.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 your event handler named
OnRowUpdating . The code receives two parameters, a reference to the object that raised the event, and a reference to a
RowUpdatingEventArgs object.
Of course, as you''re using the objects from the
System .
Data .
OleDb namespace in this example, you actually get an
OleDbRowUpdatingEventArgs object. If you were using the objects from, for example, the
System .
Data .
SqlClient namespace you would get a reference to a
SqlDbRowUpdatingEventArgs object. The
RowUpdatingEventArgs object provides a series of fields or properties that contain useful information about the event, as shown in the table:
Property | Description |
---|---|
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. |
The example page''s event handler collects the statement type by querying the
StatementType enumeration (one of the values
Delete ,
Insert ,
Select , or
Update ) 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 your
DataSet .
So, you 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 your page:
Sub OnRowUpdating(objSender As Object, _
objArgs As OleDbRowUpdatingEventArgs)
''get the text description of the StatementType
Dim 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
Case "Insert"
strISBNValue = objArgs.Row("ISBN", DataRowVersion.Current)
Case Else
strISBNValue = objArgs.Row("ISBN", DataRowVersion.Original)
End Select
''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, your
OnRowUpdated event handler will be executed. In this case, you get a reference to a
RowUpdatedEventArgs object instead of a
RowUpdatingEventArgs object. It exposes the same five properties as the
RowUpdatingEventArgs class, plus two more useful fields, as shown in the following table:
Property | Description |
---|---|
Errors | An Error object containing details of any error that was generated by the data provider when executing the update. |
RecordsAffected | The number of rows that were changed, inserted, or deleted by execution of the SQL statement. Expect one ( 1 ) on success and zero or -1 if there is an error. |
So, in your
OnRowUpdated event handler, you can provide information about what happened after the update. Collect the statement type again; 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 .
And this time you can also include details about the result of the update. You can query the
RecordsAffected value to see if a row was updated (as we expect), and if not include the error message from the
Errors field.
''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)
Case "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 & "'' " _
& "Title=''" & strTitleOriginal & "'' " _
& "PublicationDate=''" & strPubDateOriginal & "''<br />" _
& "* Current values: ISBN=''" & strISBNCurrent & "'' " _
& "Title=''" & strTitleCurrent & "'' " _
& "PublicationDate=''" & strPubDateCurrent & "''<br />"
''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
One 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 the example), the
Current and
Original values are available in both the
RowUpdating and the
RowUpdated events. Therefore you can use these events to monitor changes and report errors (more in a later example).
The techniques 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.