One way to avoid the "lost update" problem, where changes are overwritten silently and the last one wins, is with an optimistic concurrency technique. This can be accomplished using a rowversion column and comparing the original and current value during each update. The update can be rejected if any of the row values changed or the row deleted.
CREATE TABLE dbo.Customer(
CustomerID int NOT NULL IDENTITY
CONSTRAINT PK_Customer PRIMARY KEY
, LastName varchar(50) NOT NULL
, FirstName varchar(50) NOT NULL
, EMailAddress varchar(255) NOT NULL
, RowVersion rowversion
);
GO
CREATE PROCEDURE dbo.UpdateCustomer
@CustomerID int
, @LastName varchar(50)
, @FirstName varchar(50)
, @EMailAddress varchar(255)
, @OriginalRowVersion rowversion
AS
SET NOCOUNT ON;
UPDATE dbo.Customer
SET
LastName = @LastName
, FirstName = @FirstName
, EMailAddress = @EMailAddress
WHERE
CustomerID = @CustomerID
AND RowVersion = @OriginalRowVersion;
IF @@ROWCOUNT = 0
BEGIN
RAISERROR ('Customer was updated or deleted by another user', 16, 1);
END;
GO
SQL Server similarly performs optimistic concurrency checks in the SNAPSHOT transaction isolation level. When a row is modified in a SNAPSHOT session, the database engine internally checks the row's current transaction sequence number to determine if it was modified outside the current snapshot transaction and raises error "Snapshot isolation transaction aborted due to update conflict" when the row has been changed or deleted. This provides the same protection against lost updates without the rowversion column, value check, and RAISERROR in the above example.