-1

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?

Bas Velden
  • 408
  • 1
  • 7
  • 21
  • 5
    Your script is wide open to [SQL Injection Attack](http://stackoverflow.com/questions/60174/how-can-i-prevent-sql-injection-in-php) Even [if you are escaping inputs, its not safe!](http://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string) Use [prepared parameterized statements](http://php.net/manual/en/mysqli.quickstart.prepared-statements.php) in either the `MYSQLI_` or `PDO` API's – RiggsFolly May 24 '19 at 12:12
  • @RiggsFolly I know it is, I'm currently creating this for a course im following and the security part will come later. The website will not be going live any time in the future. I do appreciate the heads up though – Bas Velden May 24 '19 at 12:14
  • Possible duplicate of [How to version control data stored in mysql](https://stackoverflow.com/questions/2751808/how-to-version-control-data-stored-in-mysql) – Raymond Nijland May 24 '19 at 12:35
  • @RaymondNijland The question seems to me to be very different. I don't need to log what changes are being made and store them in a different table. – Bas Velden May 24 '19 at 12:55
  • 1
    *"The question seems to me to be very different. I don't need to log what changes are being made and store them in a different table"* well you basically want version control here, as there isn't a real native solution in MySQL which you can use.. Mayve the question i've linked wasn't the best one try the search [mysql version control](https://stackoverflow.com/search?q=mysql+version+control) – Raymond Nijland May 24 '19 at 13:05

2 Answers2

2

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.

Peter
  • 29,454
  • 5
  • 48
  • 60
  • 1
    +1 but for extra safety and sanity check the update might/can even be `UPDATE products SET img_url = ?, version = version + 1 WHERE product_id = ? AND version = ;` as your own session can still update the version.. – Raymond Nijland May 24 '19 at 13:11
  • 1
    @RaymondNijland, FOR UPDATE blocks updates from other *transactions*, not just sessions (and I don't believe MySQL supports concurrent transactions in one session anyway). That being said, the conditional update is a viable alternative if transaction are not possible for some reason. One would have to check if zero or one rows have been updated, essentially a compare-and-swap. I've added that to the answer; thanks for the reminder. – Peter May 24 '19 at 13:26
  • *" FOR UPDATE blocks updates from other transactions, not just sessions (and I don't believe MySQL supports concurrent transactions in one session anyway)"* Yup sorry for the confusion i meant transaction and not session in mine other comment. *" I've added that to the answer; thanks for the reminder."* No problem. – Raymond Nijland May 24 '19 at 13:30
0

consider below points 1) u want multiple admin to edit different products 2) u do not want multiple admin to edit same product

3) in your code u saved product as a single table. In a real e commerce site product information is split into several tables

to achieve this u have to set up application wide mysql locks

mysql> select get_lock('product_id_12345', 5);
+---------------------------------+
| get_lock('product_id_12345', 5) |
+---------------------------------+
|                               1 |
+---------------------------------+
1 row in set (0.00 sec)

above code sets up a lock using a product_id, if some other connection tries to get lock using the same product_id, u will get 0 as response - indicating that some other user is updating the same product_id

mysql> 
mysql> select IS_FREE_LOCK('product_id_12345');
+----------------------------------+
| IS_FREE_LOCK('product_id_12345') |
+----------------------------------+
|                                0 |
+----------------------------------+
1 row in set (0.00 sec)

mysql> 
mysql> select IS_USED_LOCK('product_id_12345');
+----------------------------------+
| IS_USED_LOCK('product_id_12345') |
+----------------------------------+
|                               46 |
+----------------------------------+
1 row in set (0.00 sec)

mysql> 
mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
|              46 |
+-----------------+
1 row in set (0.00 sec)

SAMPLE ALGORITHM

<?php
ini_set('display_errors', 1);

function updateProduct() {

$user = 'root';
$pass = 'xxx';
$DB = 'test';
$host = 'localhost';

  try 
  {
    $conn = new mysqli($host, $user, $pass, $DB);


    $data['productId'] = 'product_id_12345';
    $data['productName'] = 'test';
    $data['productDesc'] = 'testing';

    $isLockFree = 'select IS_USED_LOCK("'.$data['productId'].'") ';
    $isLockFreeResult = mysqli_query($conn, $isLockFree);
    $row=mysqli_fetch_row($isLockFreeResult);

    if(empty($row[0]))
    {
        $lock = 'select get_lock("'.$data['productId'].'")';
        $result = mysqli_query($conn, $lock);

        echo 'lock established';

        $query = "UPDATE products
                  SET product_name='".$data['productName']."', 
                      product_desc='".$data['productDesc']."', 
                      price=".$data['price']."
                  WHERE 
                  product_id=".$data['productId'];

        $result = mysqli_query($conn, $query);
        if ($result == false) 
        {
          throw new Exception('Failed to execute: '. $query . 'Error: '. mysqli_error($conn));
        }
    }
    else
    {
        echo 'sorry! could not lock. somebody is updating the product info';
    }
  } 
  finally 
  {
    mysqli_close($conn);
  }
}
updateProduct();
?>
  • 1
    "A lock [...] is released [...] when your session terminates", that is, at the latest after the PHP script terminates. In other words, the lock won't be held while Alice has the form open, so show won't notice anything even if Bob modifies the row in the meantime. Locking is not the answer here anyway. Imagine what happens if Alice goes for lunch, with the form open but no intention of changing anything. – Peter May 24 '19 at 12:55
  • @Peter no body locks table when a user is in the process of filling a form. Lock is initiated only after Alice completes the form and hits submit button. If Alice opens the form, goes for lunch in the middle and then he decides to go home, then no body has access to change the product info until he returns tomorrow! – Raja vikraman May 24 '19 at 13:17
  • In real time, we have thousands of users update same data at a time, to prevent this i proposed locking concept. have i misunderstood any where?? please correct me if iam wrong.. – Raja vikraman May 24 '19 at 13:25
  • @peter is on to something here but still what happens if they press submit at the same time most likely this method is prone to race conditions.. – Raymond Nijland May 24 '19 at 13:26
  • @RaymondNijland nice hint about race condition. I will test it with apachebenchmark tool and see how it reacts. – Raja vikraman May 24 '19 at 13:32
  • *"nice hint about race condition. I will test it with apachebenchmark tool and see how it reacts."* it might be ok but `$isLockFree = 'select IS_USED_LOCK("'.$data['productId'].'") '; $isLockFreeResult = mysqli_query($conn, $isLockFree); $row=mysqli_fetch_row($isLockFreeResult); if(empty($row[0])) {` **seams** to be prone if there is a problem it will be in that part. – Raymond Nijland May 24 '19 at 13:33
  • @RaymondNijland $ ab -n 1000 -c 500 http://localhost/myWork/testPHP/get_lock_mysql.php This is ApacheBench, Version 2.3 <$Revision: 1528965 $> Benchmarking localhost (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests. triggered 1000 request in a second, not even 1 matched a race around conditon – Raja vikraman May 24 '19 at 13:50
  • i doubt ApacheBench can detect a race condition, race conditions are very hard to detec with automatic tools if not impossible but still.. Also i know for sure this method is unsafe to use when the MySQL servers are using replication as it's is mentioned in the [manual](https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html#function_is-used-lock) – Raymond Nijland May 24 '19 at 14:03
  • @RaymondNijland I accept, it is bad when replicating, but only with statement based replication. which is very old format of replication. Recent days we only use Row based replication with GTID'S... Nice to have a discussion. – Raja vikraman May 24 '19 at 14:10