Updating data via the DataSet can be tricky. In a previous version of ADO, the update was done row by row manually by looping through each row and running the relevant query. You can still do this with DataSet. You can reference each row of a DataTable in a DataSet, loop through each one, and run the appropriate query. However, the update method of the DataAdapter can take care of all the required looping for you.
There is still one problem with the update method. It does all updates, including deletes, inserts, and, obviously, updates, in the order that it retrieves the rows from the DataTable. It checks the RowState property and does the required INSERT, DELETE, or UPDATE command. If you require updates to be done in a particular order, you must filter the DataSet. In the case study, we need to control the order of the updates for the Order Details and Orders tables. You need to create separate update methods to take care of the deletes, updates, and inserts for both tables.
Update Methods |
Description |
---|---|
UpdateOrderDetails |
Parameter: |
InsertOrderDetails |
Parameter: |
DeleteOrderDetails |
Parameter: |
UpdateOrders |
Parameter: |
InsertOrders |
Parameter: |
DeleteOrders |
Parameter: |
The update functions all follow a similar pattern—they initialize variables, define the query string, define parameters, and finally execute the query string, including trapping any errors that may arise.
Let’s look at the simpler update methods: the update methods for the Order Details table. They are simpler because the Order Details table is a child table and does not have dependent tables.
The DeleteOrderDetails method is the simplest of the three because it requires fewer parameters. In fact, you only need to supply parameters for the primary key columns, OrderID and ProductID. As is standard, the first step is to declare the method and the required variables.
Protected Function DeleteOrderDetails _ (ByRef OrderDetailsTB As DataTable) 'Declare SqlConnection object for connection to database 'Use the Global Private string initialized in 'MyBase.New() Dim objConn As New SqlConnection(SQLConnectionString) 'Declare the DataAdapter that will be 'used to populate the DataSet. Dim OrderDataAdapter As New SqlDataAdapter() OrderDataAdapter.DeleteCommand = New SqlCommand() 'Declare the DataAdapter that will be 'used to populate the DataSet. Dim workParm As SqlParameter 'Declare the query string 'that will perform the update Dim DeleteOrderDetailsSTR As String = _ "DELETE FROM [Order Details] " & _ " WHERE " & _ "OrderID = @OrderID AND " & _ "ProductID = @ProductID "
Notice that we pass along a DataTable instead of a DataSet as a parameter. This is possible because the method will not be exposed as a web method. Furthermore, these update methods will only be used by other methods in the class or by descendants of the class, so we declare it as protected. The initialization also includes the query string required for the delete. Make a note of the parameters used because in the next section, we need to define these parameters.
'Create new parameter and reference it 'Set the source and version workParm = OrderDataAdapter.DeleteCommand. _ Parameters.Add("@OrderID", SqlDbType.Int) workParm.SourceColumn = "OrderID" workParm.SourceVersion = DataRowVersion.Original workParm = OrderDataAdapter.DeleteCommand. _ Parameters.Add("@ProductID", SqlDbType.Int) workParm.SourceColumn = "ProductID" workParm.SourceVersion = DataRowVersion.Original
We use the reference variable workParm to refer to each parameter that we create to reduce the amount of code we have to type. When a parameter is created, we also define the data type that is used in the database. In the database, both OrderID and ProductID are integers represented in the code by the enumerated data type SqlDbType.Int. We define the source of the parameter as a column in the DataTable. Next, we define which version of the row to use. Since we are going to delete the row, we need to make sure that we use the original version of the key columns so that we refer to the right row. If we use the default version, which is DataRowVersion.Current, we might end up deleting the wrong records. This can happen if the columns were changed and deleted in the DataSet.
After we have done all the required settings, we can then go ahead and do the update.
'Set command test and connection OrderDataAdapter.DeleteCommand. _ CommandText = DeleteOrderDetailsSTR OrderDataAdapter.DeleteCommand.Connection = objConn Try 'Open the Connection to database objConn.Open() 'Next process updates. DeleteOrderDetails = OrderDataAdapter. _ Update(OrderDetailsTB.Select _ (Nothing, Nothing, DataViewRowState.Deleted)) Catch ConcurncyErr As DBConcurrencyException Throw ConcurncyErr Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally 'Close the Connection to database objConn.Close() End Try End Function
This part of the code is more or less standard. All we do is add the query string to the DeleteCommand.commandtext property, set the connection object to use, and do the update. We filter rows in the DataTable only to pass the deleted rows using the select method of the DataTable and the DataViewRowState.Deleted row state filter. This will ensure that only deleted rows are passed to the update method of the DataAdpater, ensuring that the update method does only deletes.
Note |
We catch the concurrency error separately from other errors. Concurrency errors occur if the query did not affect any row. Because of the disconnected nature of the DataSet and Web Services, it is possible that the rows in the database, in this case, have been deleted by another user after the rows were last retrieved by the current user. I will discuss concurrency in more detail later in this chapter. |
The InsertOrderDetails method follows the same pattern as DeleteOrderDetails, but this time the query is slightly more complicated.
Dim InsertOrderDetailsSTR As String = _ "INSERT INTO [Order Details]" & _ "(OrderID,ProductID,UnitPrice,Quantity,Discount) " & _ "SELECT @OrderID,@ProductID," & _ "@UnitPrice,@Quantity,@Discount " & _ "FROM Orders ,Products " & _ "where Orders.OrderID = @OrderID " & _ "AND Products.ProductID = @ProductID " & _ "AND @ProductID not in " & _ "( SELECT ProductID " & _ "from [Order Details] " & _ "where [Order Details].OrderID = @OrderID)"
The SQL INSERT command is used with a SELECT to restrict the insert. The SELECT returns the list of insert parameters only if the following conditions are true:
An order with the given OrderID exists.
A product with the given ProductID exists.
The given ProductID is not already part of the Order Details for the given OrderID.
These conditions are mainly for concurrency reasons, to avoid inserts of products that have already been added to the order since the order was last retrieved. It also prevents addition if the order or the product no longer exists. As for the parameter version, we now use current versions for all parameters because we are dealing with new rows.
workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@OrderID", SqlDbType.Int) workParm.SourceColumn = "OrderID" workParm.SourceVersion = DataRowVersion.Current workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ProductID", SqlDbType.Int) workParm.SourceColumn = "ProductID" workParm.SourceVersion = DataRowVersion.Current workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@UnitPrice", SqlDbType.Money) workParm.SourceColumn = "UnitPrice" workParm.SourceVersion = DataRowVersion.Current workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@Quantity", SqlDbType.SmallInt) workParm.SourceColumn = "Quantity" workParm.SourceVersion = DataRowVersion.Current workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@Discount", SqlDbType.Real) workParm.SourceColumn = "Discount" workParm.SourceVersion = DataRowVersion.Current
You do not need to specify the SourceVersion as DataRowVersion.Current since this is the default value, but doing so helps in the clarity of the code and removes ambiguity.
The update is similar to that of DeleteOrder, except this time we filter for the inserted rows.
OrderDataAdapter.InsertCommand.CommandText = InsertOrderDetailsSTR OrderDataAdapter.InsertCommand.Connection = objConn Try 'Open the Connection to database objConn.Open() 'Next process updates. InsertOrderDetails = OrderDataAdapter.Update( _ OrderDetailsTB.Select( _ Nothing, Nothing, DataViewRowState.Added)) Catch ConcurncyErr As DBConcurrencyException Throw ConcurncyErr Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally 'Close the Connection to database objConn.Close() End Try
To get the new rows from the DataTable, we use DataViewRowState.Added as the filter criteria.
The UpdateOrderDetails method is slightly more tricky than the DeleteOrderDetails and the InsertOrderDetails methods. First, UpdateOrderDetails is declared and variables are initialized.
Public Function UpdateOrderDetails _ (ByVal OrderDetailsTB As DataTable) As Integer 'Declare SqlConnection object for connection to database 'Use the Global Private string initialized in 'MyBase.New() Dim objConn As New SqlConnection(SQLConnectionString) 'Declare the DataAdapter that will be 'used to populate the DataSet. Dim OrderDataAdapter As New SqlDataAdapter() OrderDataAdapter.UpdateCommand = New SqlCommand() Dim workParm As SqlParameter Dim UpdateOrderDetailsSTR As String = _ "UPDATE [Order Details] " & _ "SET UnitPrice = @NewUnitPrice, " & _ " Quantity = @NewQuantity, " & _ " Discount = @NewDiscount " & _ " WHERE OrderID = @OrigOrderID AND" & _ " ProductID = @OrigProductID"
The query string is simple, but do make a note that only the non-primary key columns are updated. The primary key columns are used to qualify the update. The tricky part is the next code section dealing with the parameters.
workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@OrigOrderID", SqlDbType.Int) workParm.SourceColumn = "OrderID" workParm.SourceVersion = DataRowVersion.Original workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@OrigProductID", SqlDbType.Int) workParm.SourceColumn = "ProductID" workParm.SourceVersion = DataRowVersion.Original workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@NewUnitPrice", SqlDbType.Money) workParm.SourceColumn = "UnitPrice" workParm.SourceVersion = DataRowVersion.Current workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@NewQuantity", SqlDbType.SmallInt) workParm.SourceColumn = "Quantity" workParm.SourceVersion = DataRowVersion.Current workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@NewDiscount", SqlDbType.Real) workParm.SourceColumn = "Discount" workParm.SourceVersion = DataRowVersion.Current
For the parameter referring to the primary keys, you must use the original version of the row. This ensures that you do not update the wrong row and provides rudimentary concurrency error checks. In effect, we have also made the primary keys read-only. As for the rest of the parameters, use the current version so that the update is applied. Afterward, all that remains is to do the update, making sure that only changed rows are passed to the update method
OrderDataAdapter.UpdateCommand.CommandText = UpdateOrderDetailsSTR OrderDataAdapter.UpdateCommand.Connection = objConn Try 'Open the Connection to database objConn.Open() 'Next process updates. UpdateOrderDetails = OrderDataAdapter.Update _ (OrderDetailsTB.Select( _ Nothing, Nothing, DataViewRowState. ModifiedCurrent)) Catch ConcurncyErr As DBConcurrencyException Throw ConcurncyErr Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally 'Close the Connection to database objConn.Close() End Try
In the update functions, there is no advance concurrency error check that checks if the rows have changed since they were last retrieved. This is a common problem in disconnected multiuser systems. To do this check, you may need to check every column and row in the table with the corresponding columns and rows in the DataTable. This, though, is not the most efficient design. The more common solution is to add a special column that you can use for tracking row versions in each table. This column, usually a datetime field or a timestamp, is changed each time any column in the particular row is modified. This can be done by the database engine through the use of triggers.
Note |
In Microsoft SQL Server 2000, the Transact-SQL timestamp data type is not the same as the timestamp data type defined in the SQL-92 standard. The SQL-92 timestamp data type is equivalent to the Transact-SQL datetime data type. Future releases of the SQL Server may modify the behavior of the timestamp data type to comply with the behavior defined in the SQL-92 standard. At that time, the current timestamp data type will be replaced with a rowversion data type. |
The special concurrency check field is always retrieved with any updateable records. When doing an update or delete, the original value of the concurrency field is used against that of the database, usually in the WHERE clause. If the original does not match the database current value, the update method will cause a concurrency exception. The DataAdapter update method throws a concurrency error if the update causes zero rows to be affected.
Thus far, we have completed the update methods that we will need to update the Order Details table. To be able to modify an order, we must also add update methods to modify the order header or, more accurately, the Orders table.
The methods follow what now is a familiar pattern for the update methods we have done so far. However, to show you the power of the DataAdapter update method, we will do the updates through stored procedures.
Note |
For illustration purposes, I have chosen not to update Order Details through stored procedures. However, in a real-world project, the use of stored procedures is advised over direct SQL query. This is mainly for performance and security purposes. It might also be a requirement imposed on you by the database administrator. |
Before we move on with the methods, we need to define the stored procedure that the method will use. You can apply the required SQL query to generate the stored procedure using the query analyzer tool that is included with Microsoft SQL Server 2000. Make sure that you apply the stored procedure to the Northwind database.
The first part of the script is to check if the stored procedure already exists. If it does, we drop the stored procedure. This part of the script is useful, especially when debugging and writing the scripts. It keeps us from having to manually drop the stored procedure each time we apply a new version.
-- ======================================= -- Script for sp_UpdateOrders -- ======================================= /*********************************************/ /* Check if a version of the stored */ /* procedure already exist and if so drop it */ /*********************************************/ IF EXISTS (SELECT name FROM sysobjects WHERE name = N'sp_UpdateOrders' AND type = 'P') DROP PROCEDURE sp_UpdateOrders GO
Next, we create the procedure with all the required parameters.
CREATE PROCEDURE sp_UpdateOrders @OrderID integer, @CustomerID nchar(5), @EmployeeID integer, @OrderDate datetime, @RequiredDate datetime, @ShippedDate datetime, @ShipVia integer, @Freight money, @ShipName nvarchar(40), @ShipAddress nvarchar(60), @ShipCity nvarchar(15), @ShipRegion nvarchar(15), @ShipPostalCode nvarchar(10), @ShipCountry nvarchar(15) AS BEGIN
Finally, we define the SQL scripts that we need to run to do the required update.
/*********************************************/ /* set up SQL to use transaction to allow */ /* rollback in case of error */ /*********************************************/ BEGIN TRANSACTION /* Run script to update Orders table */ UPDATE Orders SET CustomerID = @CustomerID, EmployeeID = @EmployeeID, OrderDate = @OrderDate, RequiredDate = @RequiredDate, ShippedDate = @ShippedDate, ShipVia = @ShipVia, Freight = @Freight, ShipName = @ShipName, ShipAddress = @ShipAddress, ShipCity = @ShipCity, ShipRegion = @ShipRegion, ShipPostalCode = @ShipPostalCode, ShipCountry = @ShipCountry WHERE Orders.OrderID = @OrderID /* check for error*/ IF (@@ERROR <> 0) GOTO Error GOTO Ok Error: /* Error Condition, so rollback*/ ROLLBACK TRANSACTION RETURN -1 GOTO Finally Ok: /*Everything is ok, so commit*/ COMMIT TRANSACTION RETURN 0 Finally: END GO
The SQL UPDATE script uses only basic concurrency checks. The stored procedure only checks for existence of the order but does not check if the order has been changed since it was last retrieved. It will override any changes made. This method is ideal only in a segregated multiuser system. In such a system, where updating is concerned, only one user can update certain rows. For instance, our system would allow only one connection per company to the Web Service, and the company will only be allowed to update its own order.
As is standard, we first do the required declaration:
-- ======================================= -- Script for sp_InsertOrders -- ======================================= /**********************************************/ /* Check if a version of the stored */ /* procedure already exists and if so drop it */ /**********************************************/ IF EXISTS (SELECT name FROM sysobjects WHERE name = N'sp_InsertOrders' AND type = 'P') DROP PROCEDURE sp_InsertOrders GO CREATE PROCEDURE sp_InsertOrders @OrderID integer output, @CustomerID nchar(5), @EmployeeID integer, @OrderDate datetime, @RequiredDate datetime, @ShippedDate datetime, @ShipVia integer, @Freight money, @ShipName nvarchar(40), @ShipAddress nvarchar(60), @ShipCity nvarchar(15), @ShipRegion nvarchar(15), @ShipPostalCode nvarchar(10), @ShipCountry nvarchar(15) AS BEGIN
Notice that the stored procedure has an output parameter. Use this technique when some column values are auto-generated and required in subsequent updates. In our case, we will need the generated value of the OrderID column for inserting related rows in Order Details.
The next step is to define a special variable “@@Now” that we will use to generate values to help retrieve the auto-generated value of OrderID. @@Now is set to the value of the current date and time. Since datetime is accurate to more than a millisecond, the chance of duplicating the value for @@Now is remote. We then do the insert, but instead of adding all values for all columns for certain datetime columns (OrderDate, RequiredDate, and ShippedDate), we set them to the value generated in @@Now.
/*********************************************/ /* declare a special variable to be used as */ /* a special marker in the inserted row so */ /* that we can retrieve the auto-generated */ /* value for the OrderID column */ /*********************************************/ DECLARE @@Now datetime SET @@Now = GETDATE() BEGIN TRANSACTION /*********************************************/ /* Run script to insert in the Orders table */ /* Use the datetime columns and @@Now to add */ /* markers to record so that we can retrieve */ /* the auto generated OrderId */ /*********************************************/ Insert INTO Orders( CustomerID, EmployeeID, OrderDate, RequiredDate, ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion, ShipPostalCode, ShipCountry ) VALUES ( @CustomerID, @EmployeeID, @@Now, @@Now, @@Now, @ShipVia, @Freight, @ShipName, @ShipAddress, @ShipCity, @ShipRegion, @ShipPostalCode, @ShipCountry ) /* check for error*/ IF (@@ERROR <> 0) GOTO Error
Once we have checked for errors with the insert and everything is fine, the next step is to retrieve the generated OrderID value. For this, OrderDate, RequiredDate, and ShippedDate are retrieval arguments since we know that only the newly inserted record will have the value of the @@Now variable for each of the above mentioned columns.
/* Retrieve auto-generated OrderID*/ SELECT @OrderID = OrderID FROM Orders WHERE OrderDate = @@Now AND RequiredDate = @@Now AND ShippedDate = @@Now /* check for error*/ IF (@@ERROR <> 0) GOTO Error
Finally, all that remains is to set OrderDate, RequiredDate, and ShippedDate to the correct value.
/*Update columns used in marker with proper value*/ UPDATE Orders SET OrderDate = @OrderDate, RequiredDate = @RequiredDate, ShippedDate = @ShippedDate WHERE OrderID = @OrderID /* check for error*/ IF (@@ERROR <> 0) GOTO Error GOTO Ok Error: /* Error Condition, so rollback*/ ROLLBACK TRANSACTION RETURN -1 GOTO Finally Ok: /*Everything is ok, so commit*/ COMMIT TRANSACTION RETURN 0 Finally: END GO
The stored procedure for delete is much simpler than the other two:
-- ======================================= -- Script for sp_DeleteOrders -- ======================================= /*********************************************/ /* Check if a version of the stored */ /* procedure already exist and if so drop it */ /*********************************************/ IF EXISTS (SELECT name FROM sysobjects WHERE name = N'sp_DeleteOrders' AND type = 'P') DROP PROCEDURE sp_DeleteOrders GO CREATE PROCEDURE sp_DeleteOrders @OrderID integer AS BEGIN /*********************************************/ /* First delete order entries for the order */ /* to avoid constraint violation error. */ /*********************************************/ BEGIN TRANSACTION DELETE FROM [Order Details] WHERE [Order Details].OrderID = @OrderID /* check for error*/ IF (@@ERROR <> 0) GOTO Error /* We can now delete the order from the Orders table */ DELETE FROM Orders WHERE Orders.OrderID = @OrderID /* check for error*/ IF (@@ERROR <> 0) GOTO Error GOTO Ok Error: /* Error Condition, so rollback*/ ROLLBACK TRANSACTION RETURN -1 GOTO Finally Ok: /*Everything is ok, so commit*/ COMMIT TRANSACTION RETURN 0 Finally: END GO
Note that the script is designed in such a way to optimize the delete process. The stored procedure will delete the whole order, including the related rows in the Order Details table, to prevent foreign key constraint violation in the database.
First, we need to declare the UpdateOrders method:
Protected Function UpdateOrders _ (ByRef OrdersTB As DataTable) _ As Integer 'Declare SqlConnection object for connection to database 'Use the Global Private string initialized in 'MyBase.New() Dim objConn As New SqlConnection(SQLConnectionString) 'Declare the DataAdapter that will be 'used to populate the DataSet. Dim OrderDataAdapter As New SqlDataAdapter() OrderDataAdapter.UpdateCommand = New SqlCommand()
Then we have to define the command text for UpdateCommand. We are going to use the stored procedures that we created. As you can see below, all you need to specify in the command text is the stored procedure name. We must also tell UpdateCommand that the commandtype is a stored procedure.
OrderDataAdapter.UpdateCommand.CommandText = "sp_UpdateOrders" OrderDataAdapter.UpdateCommand.Connection = objConn OrderDataAdapter.UpdateCommand.CommandType = _ CommandType.StoredProcedure
Next, we need to define all the parameters that the stored procedure uses. The return value from the stored procedure is also captured in a parameter. This is a simple matter of setting the parameter direction to ParameterDirection.ReturnValue.
'Declare variable to be used as a 'reference to Parameters Dim workParm As SqlParameter 'return value workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@return", SqlDbType.Int) workParm.Direction = ParameterDirection.ReturnValue
Remember that the OrderID is read-only, so we choose the original value and ignore the current value in case it has been changed.
'parameter 1 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@OrderID", SqlDbType.Int) workParm.SourceColumn = "OrderID" workParm.SourceVersion = DataRowVersion.Original
For the other parameters, since they are updateable, we choose the current value to apply any change to the database.
'parameter 2 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@CustomerID", SqlDbType.NChar, 5) workParm.SourceColumn = "CustomerID" workParm.SourceVersion = DataRowVersion.Current 'parameter 3 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@EmployeeID", SqlDbType.Int) workParm.SourceColumn = "EmployeeID" workParm.SourceVersion = DataRowVersion.Current 'parameter 4 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@OrderDate", SqlDbType.DateTime) workParm.SourceColumn = "OrderDate" workParm.SourceVersion = DataRowVersion.Current 'parameter 5 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@RequiredDate", SqlDbType.DateTime) workParm.SourceColumn = "RequiredDate" workParm.SourceVersion = DataRowVersion.Current 'parameter 6 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShippedDate", SqlDbType.DateTime) workParm.SourceColumn = "ShippedDate" workParm.SourceVersion = DataRowVersion.Current 'parameter 7 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipVia", SqlDbType.Int) workParm.SourceColumn = "ShipVia" workParm.SourceVersion = DataRowVersion.Current 'parameter 8 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@Freight", SqlDbType.Money) workParm.SourceColumn = "Freight" workParm.SourceVersion = DataRowVersion.Current 'parameter 9 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipName", SqlDbType.NVarChar, 40) workParm.SourceColumn = "ShipName" workParm.SourceVersion = DataRowVersion.Current 'parameter 10 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipAddress", SqlDbType.NVarChar, 60) workParm.SourceColumn = "ShipAddress" workParm.SourceVersion = DataRowVersion.Current 'parameter 11 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipCity", SqlDbType.NVarChar, 15) workParm.SourceColumn = "ShipCity" workParm.SourceVersion = DataRowVersion.Current 'parameter 12 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipRegion", SqlDbType.NVarChar, 15) workParm.SourceColumn = "ShipRegion" workParm.SourceVersion = DataRowVersion.Current 'parameter 13 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipPostalCode", SqlDbType.NVarChar, 10) workParm.SourceColumn = "ShipPostalCode" workParm.SourceVersion = DataRowVersion.Current 'parameter 14 workParm = OrderDataAdapter.UpdateCommand. _ Parameters.Add("@ShipCountry", SqlDbType.NVarChar, 15) workParm.SourceColumn = "ShipCountry" workParm.SourceVersion = DataRowVersion.Current
We are now ready to do the update, passing only changed rows to the update method.
Try 'Open the Connection to database objConn.Open() 'Next process updates. UpdateOrders = OrderDataAdapter.Update _ (OrdersTB.Select( _ Nothing, Nothing, DataViewRowState.ModifiedCurrent)) Catch ConcurncyErr As DBConcurrencyException Throw ConcurncyErr Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally 'Close the Connection to database objConn.Close() End Try End Function
The DeleteOrders method follows a pattern similar to UpdateOrders.
Protected Function DeleteOrders _ (ByRef OrdersTB As DataTable) _ As Integer 'Declare SqlConnection object for connection to database 'Use the Global Private string initialized in 'MyBase.New() Dim objConn As New SqlConnection(SQLConnectionString) 'Declare the DataAdapter that will be 'used to populate the DataSet. Dim OrderDataAdapter As New SqlDataAdapter() OrderDataAdapter.DeleteCommand = New SqlCommand() Dim workParm As SqlParameter OrderDataAdapter.DeleteCommand.CommandText = "sp_DeleteOrders" OrderDataAdapter.DeleteCommand.Connection = objConn OrderDataAdapter.DeleteCommand.CommandType = _ CommandType.StoredProcedure 'return value workParm = OrderDataAdapter.DeleteCommand. _ Parameters.Add("@return", SqlDbType.Int) workParm.Direction = ParameterDirection.ReturnValue 'parameter 1 workParm = OrderDataAdapter.DeleteCommand. _ Parameters.Add("@OrderID", SqlDbType.Int) workParm.SourceColumn = "OrderID" workParm.SourceVersion = DataRowVersion.Original Try 'Open the Connection to database objConn.Open() ' Next process updates. DeleteOrders = OrderDataAdapter.Update( _ OrdersTB.Select( _ Nothing, Nothing, DataViewRowState.Deleted)) Catch ConcurncyErr As DBConcurrencyException Throw ConcurncyErr Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally 'Close the Connection to database objConn.Close() End Try End Function
The big difference from UpdateOrders is the smaller number of parameters required.
The InsertOrders Method also follows the same pattern. However, the parameter direction for the OrderID is now set to ParameterDirection.Output because we will need this value in the DataSet to update related records in the Order Details table.
Protected Function InsertOrders _ (ByRef OrdersTB As DataTable) _ As Integer 'Declare SqlConnection object for connection to database 'Use the Global Private string initialized in 'MyBase.New() Dim objConn As New SqlConnection(SQLConnectionString) 'Declare the DataAdapter that will be 'used to populate the DataSet. Dim OrderDataAdapter As New SqlDataAdapter() OrderDataAdapter.InsertCommand = New SqlCommand() OrderDataAdapter.InsertCommand.CommandText = "sp_InsertOrders" OrderDataAdapter.InsertCommand.Connection = objConn OrderDataAdapter.InsertCommand.CommandType = _ CommandType.StoredProcedure
We also need to instruct UpdateCommand to update data in the DataSet using the return output parameter. This is useful because we will only know the true value of OrderID after we have done the insert. Furthermore, using foreignkey constraint, we can cascade this update to include related child rows in the Order Details DataTable.
'Instruct InsertComand to use outparameter to update 'the dataset. OrderDataAdapter.InsertCommand.UpdatedRowSource = _ UpdateRowSource.OutputParameters
All that remains now is to continue with the same pattern as with the other order update methods.
Dim workParm As SqlParameter 'return value workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@return", SqlDbType.Int) workParm.Direction = ParameterDirection.ReturnValue 'parameter 1 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@OrderID", SqlDbType.Int) workParm.SourceColumn = "OrderID" workParm.Direction = ParameterDirection.Output 'parameter 2 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@CustomerID", SqlDbType.NChar, 5) workParm.SourceColumn = "CustomerID" workParm.SourceVersion = DataRowVersion.Current 'parameter 3 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@EmployeeID", SqlDbType.Int) workParm.SourceColumn = "EmployeeID" workParm.SourceVersion = DataRowVersion.Current 'parameter 4 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@OrderDate", SqlDbType.DateTime) workParm.SourceColumn = "OrderDate" workParm.SourceVersion = DataRowVersion.Current 'parameter 5 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@RequiredDate", SqlDbType.DateTime) workParm.SourceColumn = "RequiredDate" workParm.SourceVersion = DataRowVersion.Current 'parameter 6 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShippedDate", SqlDbType.DateTime) workParm.SourceColumn = "ShippedDate" workParm.SourceVersion = DataRowVersion.Current 'parameter 7 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipVia", SqlDbType.Int) workParm.SourceColumn = "ShipVia" workParm.SourceVersion = DataRowVersion.Current 'parameter 8 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@Freight", SqlDbType.Money) workParm.SourceColumn = "Freight" workParm.SourceVersion = DataRowVersion.Current 'parameter 9 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipName", SqlDbType.NVarChar, 40) workParm.SourceColumn = "ShipName" workParm.SourceVersion = DataRowVersion.Current 'parameter 10 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipAddress", SqlDbType.NVarChar, 60) workParm.SourceColumn = "ShipAddress" workParm.SourceVersion = DataRowVersion.Current 'parameter 11 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipCity", SqlDbType.NVarChar, 15) workParm.SourceColumn = "ShipCity" workParm.SourceVersion = DataRowVersion.Current 'parameter 12 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipRegion", SqlDbType.NVarChar, 15) workParm.SourceColumn = "ShipRegion" workParm.SourceVersion = DataRowVersion.Current 'parameter 13 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipPostalCode", _ SqlDbType.NVarChar, 10) workParm.SourceColumn = "ShipPostalCode" workParm.SourceVersion = DataRowVersion.Current 'parameter 14 workParm = OrderDataAdapter.InsertCommand. _ Parameters.Add("@ShipCountry", SqlDbType.NVarChar, 15) workParm.SourceColumn = "ShipCountry" workParm.SourceVersion = DataRowVersion.Current Try 'Open the Connection to database objConn.Open() 'Next process updates. InsertOrders = OrderDataAdapter.Update( _ OrdersTB.Select( _ Nothing, Nothing, DataViewRowState.Added)) Catch ConcurncyErr As DBConcurrencyException Throw ConcurncyErr Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally 'Close the Connection to database objConn.Close() End Try End Function
Now that we have completed all the methods required for updating the Orders and Order Details tables, we can move on to expose the update functions to the Web Service clients. For the web method, we will only need to expose one method that does all the updates. The advantage of doing one method is to reduce the number of calls that have to be made to the Web Service. The disadvantage is that we are passing a bigger set of data at once, and it is more difficult to recover from update errors.
The signature for our method is shown here:
Class Method Name |
Web Service Method Name |
Description |
---|---|---|
FullUpdateOrder |
FullUpdateOrder |
Parameter: |
The first step is not the declaration of variables, as is usually the case, but rather we first check if the given DataSet contains the two required DataTables for the update. If it does, then we provide reference variables to them.
<WebMethod()> _ Public Function FullUpdateOrder _ (ByRef Orders As DataSet) _ As String 'check that the Order table is available in the dataset If Not Orders.Tables.Contains("Orders") Then Return "Order table not in dataset" End If 'check that the Order Details table is available in 'the dataset If Not Orders.Tables.Contains("Order Details") Then Return "Order Details table not in dataset" End If 'reference the two tables for easier access Dim OrdersTB As DataTable = Orders.Tables("Orders") Dim OrderDetailsTB As DataTable = Orders.Tables("Order Details")
We now need to make sure that there is a foreign key constraint between the two DataTables with cascading update.
'Make sure a foreign key between parent table (Orders) 'and child table (Order Details) exists 'Create foreign key Dim fk_orders As ForeignKeyConstraint = _ New ForeignKeyConstraint("fk_orders", _ OrdersTB.Columns("OrderID"), _ OrderDetailsTB.Columns("OrderID")) fk_orders.DeleteRule = Rule.Cascade fk_orders.UpdateRule = Rule.Cascade ' Add new foreign key constraint Try OrderDetailsTB.Constraints.Add(fk_orders) Catch err As Exception 'If cannot add constraint, then 'it must already exist or some other 'restrictions exist 'We can still try to update though End Try
The key is only vital if we are going to do inserts.
We are now ready to begin the updates. First, we take care of the deletes. To avoid concurrency errors in the DataSet, we delete the child records first. If we do not do so, the DeleteOrders method will still work. However, when we later run the DeleteOrderDetails method, it will fail because child records marked as deleted in the DataSet (due to cascade delete) will no longer exist in the database and a concurrency error would be thrown. In other words, the DataSet will be out of sync with the database.
'The first step is to do the deletion 'Remember that must delete child first 'to avoid concurrency issue in dataset Try DeleteOrderDetails(OrderDetailsTB) Catch Err As Exception Return "Error deleting rows in OrderDetails, Message:" Err.Message End Try Try DeleteOrders(OrdersTB) Catch Err As Exception Return "Error deleting rows Orders, Message:" _ & Err.Message End Try
Next, we do the inserts and the updates:
'Do Inserts 'With insert, the parent has to be inserted first 'to avoid concurrency error Try InsertOrders(OrdersTB) Catch Err As Exception Return "Error inserting rows Orders, Message:" & Err.Message End Try Try InsertOrderDetails(OrderDetailsTB) Catch Err As Exception Return "Error inserting rows in OrderDetails, Message:" & Err.Message End Try 'Finally do the updates Try UpdateOrders(OrdersTB) Catch Err As Exception Return "Error updating rows Orders, Message:" & Err.Message End Try Try UpdateOrderDetails(OrderDetailsTB) Catch Err As Exception Return "Error updating rows in OrderDetails, Message:" & Err.Message End Try Return "" End Function
The method will return error messages if any exceptions are thrown. Otherwise, an empty string is returned.
Since these methods take DataSet as a parameter, the Web Server cannot automatically generate test pages. To do testing, you have to either create test methods, which you then comment out when you move from production to live system, or you can create a test application that uses the Web Service.
In the source code on the companion CD, you will find some test methods that you can use. Also included are additional methods that return employees and products.