Professional ASP.NET 1.1 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Professional ASP.NET 1.1 [Electronic resources] - نسخه متنی

Alex Homeret

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید








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.


/ 244