Update record based on rowversion value?

2019-05-21 12:36发布

问题:

I have recently implemented SQL rowversion to prevent concurrency issues in my system. I use rowversion in where clause when updating single rows in the tables. So far I have tested and seems like a good solution. Now I'm looking for an easy way to implement this feature in my system. Here is SP that runs when user wants to update the record:

CREATE PROCEDURE [dbo].[UpdateBuilding]
    @Status BIT = NULL,
    @Name VARCHAR(50) = NULL,
    @Code CHAR(2) = NULL,
    @OriginalRowVersion ROWVERSION
AS
    SET NOCOUNT ON
    SET XACT_ABORT ON
    BEGIN
        UPDATE dbo.Building
        SET Status = @Status,
            Name = @Name,
            Code = @Code,
            ActionDt = CURRENT_TIMESTAMP
        WHERE RowVersion = @OriginalRowVersion
        IF @@ROWCOUNT = 0
        BEGIN
            RAISERROR('Buildingwith code %s was modified or deleted by another user.', 16, 1, @Code);
        END;
    END;

If I want to execute SP above I would need to pass required parameters. This is how I call SP in SQL Management Studio:

EXEC UpdateBuilding
    @Status = 1,
    @Name = "Rockefeller Center",
    @Code = 436,
    @OriginalRowVersion = 0x0000000000006955;

Now I started looking how to implement this in my system where I use ColdFusion to communicate with Datatbase. Here is example on how this procedure will be executed with CF 2016:

<cfstoredproc procedure="UpdateBuilding" datasource="#dsn#">
    <cfprocparam dbvarname="@Status" value="#trim(arguments.status)#" cfsqltype="cf_sql_bit" />
    <cfprocparam dbvarname="@Code" value="#trim(arguments.code)#" cfsqltype="cf_sql_char" maxlength="2" null="#!len(trim(arguments.code))#" />
    <cfprocparam dbvarname="@Name" value="#trim(arguments.name)#" cfsqltype="cf_sql_varchar" maxlength="50" null="#!len(trim(arguments.name))#" />
    <cfprocresult name="Result"/>
</cfstoredproc>

You can see that all values are passed with arguments that user submits in the form. However, updating based on PK value (Code column in my case) was pretty simple. Now I have binary value and that makes everything more complicated. First I use JSON to send the data to client side. Sending rowversion in JSON object would require converting that value to binary and then converting back when user submits the form. I'm wondering if there is better way to achieve this? Ideally I would not even send rowversion value to the user side. I woul keep that on the back end and once user submits the form pull row version value based on PK then call stored procedure. If anyone knows good way to handle this kind of situations please let me know. I have not used rowversion before and this is new to me.

回答1:

I use a similar approach where I have a column named version of type int. I pass that to the client in any read operation. If the client updates a record, it must send back the version of the record being updating, and the update will increment the version number. However, my approach sets a ColdFusion lock instead of a DB lock. Here's some simplified logic:

function updateRecord (
    required numeric recordID,
    required struct updateData,
    required numeric version) {

    lock name="#arguments.recordID#" type="exclusive" timeout="1" throwontimeout=true {
        qRecord = queryExecute() // get the record
        if (qRecord.recordCount != 1) {
            throw();
        }
        if (qRecord.version != arguments.version) {
            throw();
        }
        // do the update using arguments.updateData
    }

}

One issue with this is that other nodes in a cluster would not be aware of the named lock. You would have to come up with another way of locking that section of code to other requests across the cluster. There are ways to do it, as described in this thread:

https://dev.lucee.org/t/distributed-lock-management/1004

And if this is an issue, I'm sure there are other solutions available.



回答2:

Dealing with binary isn't as difficult as you may be thinking. After you retrieve the binary value, encode it as a string suitable for sending to the client (either hex or base64):

<cfset hexStringForClient = binaryEncode(queryName.theBinaryColumn, 'hex')>

When the client side returns the encoded string, decode it back into binary and use cf_sql_binary to pass it to the database:

<cfset binaryValue = binaryDecode(hexStringFromClient, 'hex')>

<cfstoredproc procedure="UpdateBuilding" datasource="#dsn#">
    ... 
    <cfprocparam dbvarname="@OriginalRowVersion" value="#binaryValue#" cfsqltype="cf_sql_binary" />
    ....
</cfstoredproc>

Side note, as mentioned in your other thread, the optimistic concurrency check uses both the primary key and rowversion value in the WHERE clause:

WHERE
    Code = @Code
    AND RowVersion = @OriginalRowVersion;