5

I'm using NHibernate with the version property that automatically increments every time my aggregate root is updated. What happens if 2 or more people update the same record at the exact same time?

Also, how would I test this?

Note that this isn't a situation I've been in, just wondering.

Daniel Schilling
  • 4,829
  • 28
  • 60
gcso
  • 2,315
  • 3
  • 28
  • 50

5 Answers5

6

What's Atomic, and What's Not

As the others have stated, updates in SQL Server are atomic operations. However, when updating data with NHibernate (or any O/RM), you typically first select the data, make your changes to the object, then update the database with your changes. That sequence of events is not atomic. Even if the select and update were performed within milliseconds of each other, the chance exists for another update to slip in the middle. If two clients fetched the same version of the same data, they could unwittingly overwrite each-other's changes if they assumed that they were the only ones editing that data at that time.

Problem Illustration

If we didn't guard against this concurrent-update scenario, weird things could happen - sneaky bugs that shouldn't seem possible. Suppose we had a class that modeled the state changes of water:
public class BodyOfWater
{
    public virtual int Id { get; set; }
    public virtual StateOfMatter State { get; set; }

    public virtual void Freeze()
    {
        if (State != StateOfMatter.Liquid)
            throw new InvalidOperationException("You cannot freeze a " + State + "!");
        State = StateOfMatter.Solid;
    }

    public virtual void Boil()
    {
        if (State != StateOfMatter.Liquid)
            throw new InvalidOperationException("You cannot boil a " + State + "!");
        State = StateOfMatter.Gas;
    }
}

Let's say the following body of water is recorded in the database:

new BodyOfWater
{
    Id = 1,
    State = StateOfMatter.Liquid
};

Two users fetch this record from the database at roughly the same time, modify it, and save the changes back to the database. User A freezes the water:

using (var transaction = sessionA.BeginTransaction())
{
    var water = sessionA.Get<BodyOfWater>(1);
    water.Freeze();
    sessionA.Update(water);

    // Same point in time as the line indicated below...

    transaction.Commit();
}

User B tries to boil the water (now ice!)...

using (var transaction = sessionB.BeginTransaction())
{
    var water = sessionB.Get<BodyOfWater>(1);

    // ... Same point in time as the line indicated above.

    water.Boil();
    sessionB.Update(water);
    transaction.Commit();
}

... and is successful!!! What? User A froze the water. Shouldn't an exception have been thrown saying "You cannot boil a Solid!"? User B fetched the data before User A had saved his changes, so to both users, the water appeared to initially be a liquid, so both users were allowed to save their conflicting state changes.

Solution

To prevent this scenario, we can add a Version property to the class and map it in NHibernate with a <version /> mapping:

public virtual int Version { get; set; }

This is simply a number that NHibernate will increment every time it updates the record, and it will check to make sure no-one else has incremented the version while we weren't watching. Instead of a concurrency-naive sql update like...

update BodyOfWater set State = 'Gas' where Id = 1;

... NHibernate will now use a smarter query like this:

update BodyOfWater set State = 'Gas', Version = 2 where Id = 1 and Version = 1;

If the number of rows affected by the query is 0, then NHibernate knows something went wrong - either someone else updated the row so that the version number is now incorrect, or someone deleted the row so that that Id no longer exists. NHibernate will then throw a StaleObjectStateException.

Special Note about Web Apps

The more time there is between the initial select of the data and the subsequent update, the greater the chance for this type of concurrency problem. Consider a typical "edit" form in a web app. The existing data for an entity is selected from the database, placed into the HTML form, and sent to the browser. The user may spend several minutes modifying the values in the form before sending it back to the server. There may be a decent chance that someone else was editing the same information at the same time, and they saved their changes before we did.

Making sure the version doesn't change during the few milliseconds we're actually saving the changes might not be enough in a scenario like this. To address this problem, you could send the version number to the browser as a hidden field along with the rest of the form fields, then check to make sure the version hasn't changed when you fetch the entity back out of the database before saving. In addition, you can limit the amount of time in-between the initial select and the final update by providing separate "view" and "edit" views instead of just using an "edit" view for everything. The less time the user spends on an "edit" view, the less chance that they'll be presented with an annoying error message saying that their changes could not be saved.

Daniel Schilling
  • 4,829
  • 28
  • 60
  • I've got a question for this. This is a great explanation and I'm wondering if you could explain one more thing. Using these methods with doing a typical ORM / Web app setup definitely will prevent bad data. What would be a good practice for the user experience? I feel like showing an error to the user and redo the process is not the best. Just wondering your thoughts on this? – Ryan Kauk Feb 08 '23 at 05:47
4

Simply put: They can't. The updates are processed in sequence. Each update is - or at least should be - atomic. Thus, the property is incremented twice.

Mike Adler
  • 1,199
  • 9
  • 24
4

Before you can update a row, you have to own the lock for that row. SQL Server locks rows in an atomic fashion. That is, only one of the competing processes can get the lock. All other potential claimants have to wait for the lock to be released.

Andomar
  • 232,371
  • 49
  • 380
  • 404
4

Depends on how the Isolation levels have been setup when transactions(if used) are used with the SQL Server. (Although it is technically impossible for a "exact same time" record editing)

Some basic information on this is available at Concurrency Series: Basics of Transaction Isolation Levels

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
1

As Mike Adler said the updates are processed in sequence. but one will fail, I think it will do this by throwing a stale object exception because the version if part of the filter to update the row.

MyTable  
Id | RowVersion | Description  
1  | 1          | this description

SQL:
1st update
Update MyTable set description = 'test', rowversion=2 where id = 1 and rowversion = 1

Result:

MyTable  
Id | RowVersion | Description  
1  | 2          | test

2nd update
Update MyTable set description = 'second update', rowversion=2 where id = 1 and rowversion = 1

nothing updated.

Nathan Fisher
  • 7,961
  • 3
  • 47
  • 68