Search code examples
phpsqladmininnodb

How to detect if 2 admins update a table row at the same time in php


The website i'm developing has an admin page to add products to the database or edit existing products in the database. If 2 admins want to edit the same product the second admin should get an alert that the product has already been/is now being updated. How could I detect in php/sql when 2 admins try to edit the same product?

I haven't really tried anything because I have no idea what to try or where to start.

here's how my function looks for updating a product in the database:

//I know this is a VERY unsafe function, this website will never go online!
FUNCTION updateProduct($data) {
  include_once 'dbconn.php';
  try {
    $conn = connectToDb();
    if ($data['imageChanged'] == false) {
      $query = "SELECT img_url FROM products WHERE product_id=".$data['productId'];
      $result = mysqli_query($conn, $query);
      if ($result == false) {
        throw new Exception('Failed to execute: '. $query . 'Error: '. mysqli_error($conn));
      }
      $imgUrl = mysqli_fetch_row($result)[0];
    } else {
      $imgUrl = $data['image'];
    }
    $query2 = "img_url='".$imgUrl."'";
    $query = "UPDATE products
              SET product_name='".$data['productName']."', product_desc='".$data['productDesc']."', price=".$data['price'].", ". $query2 . " 
              WHERE product_id=".$data['productId'];
    $result = mysqli_query($conn, $query);
    if ($result == false) {
      throw new Exception('Failed to execute: '. $query . 'Error: '. mysqli_error($conn));
    }
  } finally {
    mysqli_close($conn);
  }
}

edit: storing the old data after an update is made is not needed nor is storing the updates being made(regarding the question this might be a duplicate of). The thing I would like to know is: if admin_1 is updating a row, and admin_2 is trying to update the same row at the time admin_1's transaction is still ongoing, how can my script detect that this is happening?


Solution

  • This is usually done by adding a version column that is updated every time the row changes. Before updating the row, check if the value is still the same as when the row was last read:

    SELECT img_url, version FROM products WHERE product_id = ?;
    -- Suppose this returns ('https://example.com/foo.jpg', 1)
    -- Remember that the current version is 1.
    

    Later:

    BEGIN;
    
    SELECT version FROM products WHERE product_id = ? FOR UPDATE;
    -- Check if version is still 1. If it's not, someone modified the row (execute ROLLBACK in this case). 
    -- Otherwise:
    UPDATE products SET img_url = ?, version = version + 1 WHERE product_id = ?;
    
    COMMIT;
    

    If for whatever reason transactions are not available, an alternative is to use a simple compare-and-swap:

    UPDATE products SET img_url = ?, version = version + 1 WHERE product_id = ? AND version = 1;
    

    If the number of updated rows is zero, the row has been modified in the meantime.

    Arguably, this is slightly quicker than the SELECT FOR UPDATED followed by UPDATE. However, having the conflicting version of the row enables much richer user feedback. With an author column you can tell who updated the row, for instance, not just that it happened.