0

I want to add 1 to a value within my database within a transaction. I want to ensure that the record is updated properly and hasn't been changed by someone else during that time.

I have the following code that I thought would work, but I can still pause during debugging, change the record in the database to something different and then it becomes inconsistent.

Here's my code:

using (var transaction = this.Context.Database.BeginTransaction())
{
    try
    {

        if (quiz.PasswordRequiredToTakeQuiz())
        {
            // Check password exists for quiz
            bool passwordIsValid = quiz.QuizPasswords.Any(x => x.Password.ToLower() == model.QuizPassword.ToLower() && !x.Deleted);
            QuizPassword quizPassword = quiz.QuizPasswords.Where(x => x.Password.ToLower() == model.QuizPassword.ToLower() && !x.Deleted).First();
            string passwordError = "Sorry the password you provided has expired or is not valid for this quiz";

            if (!passwordIsValid)
            {
                ViewData.ModelState.AddModelError("QuizPassword", passwordError);
            }
            else
            {
                // Password is valid for use with this quiz, but can it be used?
                if (quizPassword.RemainingUses < 1 && quizPassword.UnlimitedUses != true)
                {
                    // Password cannot be used
                    ViewData.ModelState.AddModelError("QuizPassword", passwordError);
                }
                else
                {
                    // Password CAN be used
                    if (!quizPassword.UnlimitedUses)
                    {
                        quizPassword.RemainingUses--;
                    }
                    // Increase use count
                    quizPassword.UseCount++;

                    this.Context.EntitySet<QuizPassword>().Attach(quizPassword);
                    this.Context.Entry(quizPassword).State = EntityState.Modified;

                    // I can change the record UseCount value in the database at this point
                    // then when it saves, it becomes inconsistent with other's use of
                    // the password

                    this.Context.SaveChanges();
                }
            }
        }

        // Commit the changes
        transaction.Commit();

    }
    catch(Exception)
    {
        transaction.Rollback();
    }
    finally
    {
        transaction.Dispose();
    }
}

Turn of events:

  1. Initially, UseCount = 0 in the database
  2. I run the code up to just before SaveChanges()
  3. I go into the database and change UseCount to 5
  4. I allow SaveChanges() to be called (Shouldn't be possible without being blocked)
  5. UseCount value in the database will be 1.

Normally I'd achieve this using SELECT FOR UPDATE to lock the record temporarily, but I was originally using PHP + MySQL.

I have read that the lock isn't possible, so I'm wondering how this can be achieved.

It's important because I don't want people to be able to use a password more than a set number of times! If it is possible for someone to change the value in the mean time, it doesn't guarantee the correct number of uses.

Luke
  • 22,826
  • 31
  • 110
  • 193
  • What's wrong with a simple `UPDATE SET SomeField=SomeField+1`? `SELECT FOR UPDATE` or trying to hold locks is a smell that indicates you are using the database the wrong way. The scalable way (ie for more than 10 users) to use databases for the last 20 years is optimistic concurrency, which checks the ROW_VERSION of a row to detect whether someone else modified it. – Panagiotis Kanavos Oct 09 '14 at 09:57
  • The reason is that I wanted to make use of entity framework rather than using raw queries... maybe raw queries is the only way to achieve this kind of behavior. – Luke Oct 09 '14 at 10:01
  • Why did you want to use an ORM in the first place? You have no OO behavior here, just data you manipulate. Besides, the problem is using the wrong concurrency model (pessimistic vs optimistic). Pessimistic concurrency (ie transactions) causes blocking and should be avoided. All data access methods work just fine with either, but performance suffers seriously with pessimistic concurrency. Finally, getting any ORM to emit a `Field+1` statement is too muck work compared with a single update statement – Panagiotis Kanavos Oct 09 '14 at 10:05
  • I chose to use ORM because it use it throughout my application to get objects from the database.. this piece of code isn't the only piece of code in my solution lol. I guess I'll have to create a stored procedure for this, because I need to check different variables and alter them depending on the situation. This code depends on two values being in a certain state as you can see – Luke Oct 09 '14 at 10:26
  • The point is - you aren't dealing with objects here. ORMs cover one scenario (mapping single rows to class instances for offline use) but are unsuitable for many other scenarios like batch processing, reporting, analytics or simple value manipulation – Panagiotis Kanavos Oct 09 '14 at 10:30
  • So the solution is a stored procedure? – Luke Oct 09 '14 at 10:31
  • Not necessarily, you can execute raw commands with [DbContext.Database.SqlCommand](http://stackoverflow.com/questions/5474264/how-to-pass-parameters-to-the-dbcontext-database-executesqlcommand-method) – Panagiotis Kanavos Oct 09 '14 at 10:45
  • This really wasn't as easy as I had hoped. I'm using MySQL for EF6 which I've realised I forgot to mention. I had to create a store procedure and then capture the result in a custom class that matched the values I was returning. I'll add my answer below. – Luke Oct 09 '14 at 14:30
  • @PanagiotisKanavos I'd really like to know your thoughts on my solution. – Luke Oct 10 '14 at 07:55

2 Answers2

1

One solution would be to do it with plain sql (ado.net) and pessimistic locking.

BEGIN TRANSACTION

SELECT usecount, unlimiteduses FROM quizpassword WITH (UPDLOCK, HOLDLOCK) WHERE id = x;

// check usecount here

// only do this if unlimitedUses == false
UPDATE quizpassword SET usecount = usecount + 1 WHERE id = x;

UPDATE quizpassword SET remaininguses = remaininguses -1 WHERE id = x;


COMMIT TRANSACTION // (lock is released)
mreiterer
  • 556
  • 1
  • 4
  • 15
  • The problem with this is that I need to check the value of UseCount before I increment it. I wonder if using raw SQL is the only way that this can be achieved. – Luke Oct 09 '14 at 10:04
  • Then add a `WHERE` statement in the update that includes it. You already check `RemainingUses` with the code you have but you never check `UseCount` – Panagiotis Kanavos Oct 09 '14 at 10:06
  • What about the minus one on the `RemainingUses`? – Luke Oct 09 '14 at 10:30
  • I got your point and modified my answer. This is still a plain sql solution. the pessimistic lock only locks the affected row and should therefore not cause drastic performance problems. – mreiterer Oct 09 '14 at 10:50
0

I created a stored procedure that returned a SELECT statement of the value I wanted to return, which is a Success int.

DROP PROCEDURE IF EXISTS UsePassword;

DELIMITER //
CREATE PROCEDURE UsePassword (QuizId INT(11), PasswordText VARCHAR(25))
BEGIN

  /* Get current state of password */
    SELECT RemainingUses, UnlimitedUses, Deleted INTO @RemainingUses, @UnlimitedUses, @Deleted FROM QuizPassword q WHERE q.QuizId = QuizId AND `Password` = PasswordText AND Deleted = 0 LIMIT 0,1 FOR UPDATE;

    IF FOUND_ROWS() = 0 OR @Deleted = 1 THEN

        /* Valid password not found for quiz */
        SET @Success = 0;   

    ELSEIF @UnlimitedUses = 1 THEN

        UPDATE QuizPassword SET UseCount = UseCount + 1 WHERE QuizId = QuizId AND `Password` = PasswordText;
        SET @Success = ROW_COUNT();

    ELSEIF @RemainingUses > 0 AND @UnlimitedUses = 0 THEN

        UPDATE QuizPassword SET UseCount = UseCount + 1, RemainingUses = RemainingUses - 1 WHERE QuizId = QuizId AND `Password` = PasswordText;
        SET @Success = ROW_COUNT();

    ELSE

        SET @Success = 0;

    END IF;

  /* Return rows changed rows */
  SELECT @Success AS Success;

END //
DELIMITER;

I had to create a new object to hold the values, I've only got one field in there, but you could put more.

// Class to hold return values from stored procedure
public class UsePasswordResult
{
    public int Success { get; set; }

    // could have more fields...
}

I reduced my final code down to this, which calls the stored procedure and assigns the values to the member variables within the object:

using (var transaction = this.Context.Database.BeginTransaction())
{
    try
    {

        if (quiz.PasswordRequiredToTakeQuiz())
        {
            // Attempt to use password
            UsePasswordResult result = this.Context.Database.SqlQuery<UsePasswordResult>("CALL UsePassword({0}, {1})", quiz.Id, model.QuizPassword).FirstOrDefault();

            // Check the result of the password use
            if (result.Success != 1)
            {
                // Failed to use the password
                ViewData.ModelState.AddModelError("QuizPassword", "Sorry the password you provided has expired or is not valid for this quiz");
            }
        }

        // Is model state still valid after password checks?
        if (ModelState.IsValid)
        {
            // Do stuff
        }

        transaction.Commit();
    }
    catch(Exception)
    {
        transaction.Rollback();
    }
    finally
    {
        transaction.Dispose();
    }
}

The values you return from the stored procedure must be exactly the same names as the ones in the class it's going to produce as the result.

Because I'm calling the procedure within my transaction using statement, the statement locks the record because I've selected it as SELECT... FOR UPDATE until transaction.Commit()/Rollback()/Dispose() is called within the code... thus preventing anyone from attempting to use the password whilst anyone else.

Luke
  • 22,826
  • 31
  • 110
  • 193