Você está na página 1de 7

Data Concurrency in ADO.

NET - Part I

When multiple users attempt to modify data at the same time, controls need to be
established in order to prevent one user's modifications from adversely affecting
modifications from simultaneous users. The system of handling what happens in
this situation is called concurrency control.

Handling Data Concurrency Using ADO.NET


How ADO.NET is detect and handle concurrency violations.

There are three common techniques for managing what happens when users try to modify the same data at
the same time: pessimistic, optimistic, and last-in wins.

There are three types of concurrency control:

• Pessimistic concurrency control - a row is unavailable to users from the time the record is
fetched until it is updated in the database.
• Optimistic concurrency control - a row is unavailable to other users only while the data is
actually being updated. The update examines the row in the database and determines whether
any changes have been made. Attempting to update a record that has already been changed,
results in a concurrency violation.
• "Last in wins" - a row is unavailable to other users only while the data is actually being
updated. However, no effort is made to compare updates against the original record; the record is
simply written out, potentially overwriting any changes made by other users since you last
refreshed the records.
There are two ways to handle the issues with optimistic concurrency. They are:

The Version Number Approach


Saving All Values Approach

Version Number Approach

In the version number approach, the record to be updated must have a column that
contains a date-time stamp or version number. The date-time stamp or a version
number is saved on the client when the record is read. This value is then made part
of the update.

One way to handle concurrency is to update only if value in the WHERE clause
matches the value on the record. The SQL representation of this approach is:
UPDATE Table1 SET Column1 = @newvalue1, Column2 = @newvalue2
WHERE DateTimeStamp = @origDateTimeStamp

Alternatively, the comparison can be made using the version number:

UPDATE Table1 SET Column1 = @newvalue1, Column2 = @newvalue2


WHERE RowVersion = @origRowVersionValue

If the date-time stamps or version numbers match, the record in the data store has
not changed and can be safely updated with the new values from the dataset. An
error is returned if they don't match. You can write code to implement this form of
concurrency checking in Visual Studio .NET. You will also have to write code to
respond to any update conflicts. To keep the date-time stamp or version number
accurate, you need to set up a trigger on the table to update it when a change to a
row occurs.

Saving All Values Approach

An alternative to using a date-time stamp or version number is to get copies of all


the fields when the record is read. The DataSet object in ADO.NET maintains two
versions of each modified record: an original version (that was originally read from
the data source) and a modified version, representing the user updates. When
attempting to write the record back to the data source, the original values in the
data row are compared against the record in the data source. If they match, it
means that the database record has not changed since it was read. In that case,
the changed values from the dataset are successfully written to the database.

Each data adapter command has a parameters collection for each of its four
commands (DELETE, INSERT, SELECT, and UPDATE). Each command has
parameters for both the original values, as well as the current (or modified) values.

Note Adding new records (the INSERT command) only requires the current values
since no original record exists and removing records (the DELETE command) only
requires the original values in order to locate the record to delete.

The following example shows the command text for a dataset command that
updates a typical Customers table. The command is specified for dynamic SQL and
optimistic concurrency.

UPDATE Customers
SET CustomerID = @currCustomerID,
CompanyName = @currCompanyName,
ContactName = @currContactName,
ContactTitle = currContactTitle,
Address = @currAddress,
City = @currCity,
PostalCode = @currPostalCode,
Phone = @currPhone,
Fax = @currFax
WHERE (CustomerID = @origCustomerID) AND (Address = @origAddress OR
@origAddress IS NULL AND Address IS NULL) AND (City = @origCity OR @origCity
IS NULL AND City IS NULL)
AND (CompanyName = @origCompanyName OR @origCompanyName IS NULL AND
CompanyName IS NULL) AND (ContactName = @origContactName OR
@origContactName IS NULL AND ContactName IS NULL) AND (ContactTitle =
@origContactTitle OR @origContactTitle IS NULL AND ContactTitle IS NULL)
AND (Fax = @origFax OR @origFax IS NULL AND Fax IS NULL) AND (Phone =
@origPhone OR @origPhone IS NULL AND Phone IS NULL) AND (PostalCode =
@origPostalCode OR @origPostalCode IS NULL AND PostalCode IS NULL);
SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City,
PostalCode, Phone, Fax
FROM Customers WHERE (CustomerID = @currCustomerID)

Note that the nine SET statement parameters represent the current values that will
be written to the database, whereas the nine WHERE statement parameters
represent the original values that are used to locate the original record.

The first nine parameters in the SET statement correspond to the first nine
parameters in the parameters collection. These parameters would have their
SourceVersion property set to Current.

The next nine parameters in the WHERE statement are used for optimistic
concurrency. These placeholders would correspond to the next nine parameters in
the parameters collection, and each of these parameters would have their
SourceVersion property set to Original.

The SELECT statement is used to refresh the dataset after the update has occurred.
It is generated when you set the Refresh the DataSet option in the Advanced SQL
Generations Options dialog box.

By default Visual Studio will create these parameters for you if you select the
Optimistic Concurrency option in the DataAdapter Configuration Wizard. It is up to
you to add code to handle the errors based upon your own business requirements.

Concurrency Control and Timestamps

When a row of data is read by an application and presented to the user for possible modification, most applications
do not hold a lock on the row. That is good because if they did hold locks while the user contemplated making changes,
many lockout situations would occur. If fact, unless the application used the Read Uncommitted isolation level, other
users could not even look at the data until the lock is released.
But if there are no locks held, how does one prevent incorrect successive updates to the same row? Let's say that a
shipping application is updating the UnitsInStock column for the Products table. What we want to prevent is this
sequence of updates:

1. User A reads row X with UnitsInStock of 6


2. User B reads row X with UnitsInStock of 5
3. User B updates row X changing UnitsInStock to 1
4. User A updates row X changing UnitsInStock to 2

The problem with allowing A's attempted update to succeed is that anything that B changed is ignored. B reduced the
inventory level by 5 to 1. A's change to UnitsInStock column should not be allowed in this situation. Not only is A's
change incorrect but I probably should not have been allowed at all because the total units shipped would be greater
than the inventory.
The answer to preventing this problem lies in the WHERE clause of the UPDATE statement. As we have seen, the
WHERE clause is used to identify the primary key to the row. It can also be used to identify the data that the client
application thinks that it is updating. If the row has been changed since the client application read the row, it should not
be allowed to apply updates. This is accomplished in one of two ways:
• A clause specifying the original value of every column is added to the WHERE clause
• A check on the timestamp column is added to the WHERE clause.

Many systems, including ADO, will write the check for the original value of every column. That technique works in
every database management system, not just SQL Server. SQL Server offers the timestamp data type, which can be used
to track when a row changes. Every time a row is updated, a timestamp (a.k.a rowversion) is updated to a new unique
value. This simplifies writing the stored procedures because we only have to pass in the timestamp, not the original
value of every column.

timestamp and rowversion


The SQL Server Books on-line discusses Microsoft's intent to change the timestamp
data type to be in line with the SQL-92 standard, which calls for the timestamp to contain
a date and time like the current SQL Server datetime data type. To accommodate this
future change the rowversion data type has been added as a synonym to timestamp. It
will be there when timestamp is changed. For that reason, I have used rowversion in the
table and procedure definitions in this article.

Pessimistic concurrency control

While in a disconnected architecture the "Pessimistic concurrency control" can not be implemented using
database locks, it can be implemented as follows:

• Using lock-bits at the table row level and maintain them in a session pool.
• Clear the lock-bits when the user leaves the page under program control. This has to be done to
free the locks as soon as possible, but there is no guarantee that this occurs.
• As the user can leave the browser or the site at any moment, we have to clear the lock-bits
stored is the session pool on Session_End.
• And as a final countermeasure, we might have a demon running on the server cleaning old locks.

We have to keep in mind that an application that holds locks for long periods is not scalable, but this
concurrency control schema might have to be implemented on some portions of the application.

"Last in wins"

To implement this concurrency control, we do not have to do anything. But this might be unacceptable in
some circumstances if different users start to access the same records frequently.
As explained at MSDN:

• User A fetches a record from the database.


• User B fetches the same record from the database, modifies it, and writes the updated record
back to the database.
• User A modifies the 'old' record (or just clicks on Accept) and writes it back to the database.

In the above scenario, the changes user B made were never seen by user A. Be sure that this situation is
acceptable if you plan to use the "Last in wins" approach of concurrency control.

Optimistic concurrency control

This article focuses on this approach and I will go deeply through it.
The optimistic concurrency control is based on a Record Version. Several users can open a page for the
same record, but only the first one to save it will succeed.

How it works

• User A fetches a record from the database along with its version, more on this later.
• User B fetches the same record from the database, also with its version and writes the updated
record back to the database, updating the version number.
• User A "tries" to write the record to the database, but as the version on the database is different
from the version hold by user A, the write fails, leaving user B's changes intact.

The record "Version"


The .NET DataSet uses the "All values" approach, comparing old vs. new values for all the fields. This
gives a version for the record, but if you are not using the DataSet, it can be a nightmare. First, you have to
consider the possible null values and the where clause has to be changed if a new field is added to the table.
Instead of "All values", we can use a single field to set the record version. For this purpose we can use a
GUID string or a date-time value. I decided to use the date-time for the following reasons:

• The GUID is not portable if we want to port the code to other platforms.
• The GUID uses 40 bytes instead of the 8 bytes used by the date.
• The date-time tell us when the record was last updated.

Checking for optimistic concurrency


When updating the record, the version has to be added to the WHERE clause:
UPDATE table SET fields = values WHERE pk
= pk_vlaue AND concurrency = concurrency_value

If the concurrency value given by the user does not match the concurrency value in the database, the record
will not be updated and 0 rows affected will be returned.
With this SELECT we have to consider that, while a user was modifying the record, another user might
have deleted the same record. In this case, the rows affected will also be 0.
If you want to give the user an accurate message about the situation, we can not tell that the record was
updated by another user if it was deleted. To solve this, we have to check if a record with the primary key
exists in the table.
SELECT COUNT(*) FROM table WHERE pk = pk_value

At this point, if the record count is zero, the record was deleted, other wise; a concurrency exception has to
be thrown.

Handling the user interface


Finally, we have to inform the user that his/her changes were not successful because another user changed
the data.
What to do now? Just display a JavaScript alert or a pop-up window?
What happens if user A changed 10 fields and user B (who saved first) only changed one? Will user A loose
all the 10 fields?
The approach implemented here, is to reload the page and replace only the data that was modified by user
B, leaving user A's changes intact as possible.

Você também pode gostar