I use PDO transaction
try {
DB::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
DB::$db->beginTransaction();
$db->prepare( insert query );
$db->execute();
$last_insert_id = $db->lastInsertId();
...
...
Multiple concurrent requests are expected on this script.
Question: is it possible that lastInsertId() return incorrect value for the user, who actually inserted the row?
(by "incorrect value" i mean: id that is inserted by some other user).
You're safe. The ID you get will be the correct one.
PDO's lastInsertId (and mysql's last_insert_id to which your PDO delegates the call in this case) gives the last autogenerated ID on a per-connection basis.
From mysql's documentation:
The ID that was generated is maintained in the server on a per-connection basis. This means that the value returned by the function to a given client is the first AUTO_INCREMENT value generated for most recent statement affecting an AUTO_INCREMENT column by that client. This value cannot be affected by other clients, even if they generate AUTO_INCREMENT values of their own. This behavior ensures that each client can retrieve its own ID without concern for the activity of other clients, and without the need for locks or transactions.
Concurrent connections will not compromise the integrity of the returned id. And as you mention in your comment, transactions have no bearing on this. Just don't forget to commit!
I'll also mention there is the unlikely possibility, if you run multiple statements on the same connection, and if your execute method throws an exception which isnt handled correctly, that lastInsertId could return the id of the last successful insert on that connection. But it can never return an ID from another user's query.
Related
On the manual it's stated
Returns the ID of the last inserted row, or the last value from a sequence object, depending on the underlying driver.
Where if there's no $name variable passed as the first parameter to PDO::LastInsertID(), it'll check the last row that was inserted into the database. The state of this function also can be determined to work or not by the drivers that are stated, if you're using MySQL you're fine as that's heavily supported, but if you're using a database that doesn't support the OID or AUTO_INCRAMENT table definitions, PDO::LastInsertId()'s most likely not going to work.
That's all good and makes sense, but how is this achieved?
I understand PDO is just another abstraction layer to database connection, but we're also talking about the mysqli_insert_id function as well.
My idea of this method was that there's a cache (not on PHP's side), which I later discovered was true when reading this bit of code from warezthebeef at gmail dot com on the manual page.
// Assume $dbh connection handle is already established
$sql = "INSERT INTO product (product_name) OUTPUT INSERTED.product_id VALUES (?)";
$sth = $dbh->prepare($sql);
$sth->execute(array('widgets'));
$temp = $sth->fetch(PDO::FETCH_ASSOC);
echo "<pre>",print_r($temp),"</pre>";
This would display:
Array
(
[product_id] => E1DA1CB0-676A-4CD9-A22C-90C9D4E81914
)
This is cause the table in the database would contain the row product_id and in the SQL you're stating to output that row after inserting, which would be the id for more common-ground terms, and if using AUTO_INCRAMENT would just be a single number. (you can read more into it by looking here) (this is also achievable with the RETURNING SQL keyword)
So, in a way if we didn't want to use the PDO::LastInsertID() method, we could just use:
$sql = "INSERT INTO table (name, country) OUTPUT INSERTED.id VALUES (?,?)";
$sth = $dbh->prepare($sql);
$sth->execute( ['jack', 'Australia'] );
$temp = $sth->fetch(PDO::FETCH_ASSOC);
echo $temp['id']
Which should output the new ID we just inserted into the database, noting that we use an AUTO_INCRAMENT primary or OID column.
So in my head (that's why I'm asking this question) PDO could be caching the data in PHP's Temp then retrieving or writing over it when the method is called, possibly adding the RETURNING id or such to it, but that's not the way it'd work since it's based on the OID column.
I answered half of the question - I'm just wondering how the method actually stores the last ID or pulls it
Thanks!
The exact implementation depends on the driver. I can only describe how it works for MySQL because that's what I'm familiar with.
As we described in the answers to How does MySqlCommand.LastInsertedId work? question:
If MySQL successfully executes a query requested by a client, then MySQL sends an OK_Packet as a response.
In the payload of the OK_Packet MySQL includes the last inserted id (see documentation linked above):
Type | Name | Description
------------|----------------|-----------------------------------
int<1> | header | [00] or [fe] the OK packet header
int<lenenc> | affected_rows | affected rows
int<lenenc> | last_insert_id | last insert-id
...
On the server no select last_insert_id() is executed to populate this value into the OK_packet. The driver retrieves the last inserted id from the packet and PDO in turn retrieves the value from the driver.
Maybe i have some stupid questions about mysql_insert_id...
I need to get the last inserted Id of a query using mysql_insert_id after mysql_query("INSERT INTO").
mysql_insert_id retrieves the generated Id by the previous query, but my question is...
One php file called test.php with the following code
mysql_query("INSERT INTO table (FirstName, LastName) VALUES ('" . $_POST['FirstName'] . "', '" . $_POST['LastName'] . "'); ")
$lastId = mysql_insert_id();
...continues some code using $lastId
What happens if the test.php file called from different places and different browsers at the SAME TIME ?
or what happens if other php file that containts INSERT INTO query called at the same time ?
What actually Id gets back the mysql_insert_id() ?
We have to do with a rare probability ?
I do not know if I become understandable...
The PHP manual states that the value of mysql_insert_id() returns only the most recently generated AUTO_INCREMENT value:
The value of the MySQL SQL function LAST_INSERT_ID() always contains the most recently generated AUTO_INCREMENT value, and is not reset between queries.
The MySQL manual states the it will return the last AUTO_INCREMENT based on a per-connection basis:
The ID that was generated is maintained in the server on a per-connection basis. This means that the value returned by the function to a given client is the first AUTO_INCREMENT value generated for most recent statement affecting an AUTO_INCREMENT column by that client. This value cannot be affected by other clients, even if they generate AUTO_INCREMENT values of their own. This behavior ensures that each client can retrieve its own ID without concern for the activity of other clients, and without the need for locks or transactions.
Additionally, you should stop using mysql_ functions as they are being deprecated.
Each client will be sitting on a separate connection. mysql_insert_id() will get the id of the last insert query, based on the passed/current connection. So you don't have to worry about multiple scripts causing problems.
Also, from the code you provided above, your script is vulnerable to a SQL injection attack. Escape your user-input or even better, use PDO/MySQLi.
You get back the autoincrement ID from the last query on your connection. It will not cross connections, so there is no harm in two scripts running simultaneously.
Side note: if you have the option, use the mysqli or PDO libraries instead of the deprecated mysql libarary.
Wouldn't there be a problem with it if for example when a user clicks on a link, a new row is automatically inserted and then the php code requests the last inserted id, and at the same time another row is inserted by another user, so the returned id is actually not the one I'm expecting..?
Am I wrong? Is there a way to do the same without that 'security' hole?
(like maybe from within the prepared statement or something...)
P.S the id is automatically generated.
Thank you.
As mentioned in the manual:
LAST_INSERT_ID() (with no argument) returns a BIGINT (64-bit) value representing the first automatically generated value that was set for an AUTO_INCREMENT column by the most recently executed INSERT statement to affect such a column. For example, after inserting a row that generates an AUTO_INCREMENT value, you can get the value like this:
mysql>SELECT LAST_INSERT_ID();
->195
The currently executing statement does not affect the value of
LAST_INSERT_ID(). Suppose that you generate an AUTO_INCREMENT value
with one statement, and then refer to LAST_INSERT_ID() in a
multiple-row INSERT statement that inserts rows into a table with its
own AUTO_INCREMENT column. The value of LAST_INSERT_ID() will remain
stable in the second statement; its value for the second and later
rows is not affected by the earlier row insertions. (However, if you
mix references to LAST_INSERT_ID() and LAST_INSERT_ID(expr), the
effect is undefined.)
If the previous statement returned an error, the value of
LAST_INSERT_ID() is undefined. For transactional tables, if the
statement is rolled back due to an error, the value of
LAST_INSERT_ID() is left undefined. For manual ROLLBACK, the value of
LAST_INSERT_ID() is not restored to that before the transaction; it
remains as it was at the point of the ROLLBACK.
So, LAST_INSERT_ID() is always transaction-safe (even though you don't use transaction).
The MySQL Server transfers the insert ID as part of the OK message after a successful INSERT. This ID is stored in PDO, therefore without a round-trip to the server PDO can return you the correct ID for your connection in a safe way.
Reference: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#OK_Packet
To counteract this you would use a transaction.
This would essentially isolate your insert from others, so as long as your Insert/lastInsertId() call is within the same transaction, it will work just fine.
The manual for mysql_insert_id() says
Because mysql_insert_id() acts on the last performed query, be sure to
call mysql_insert_id() immediately after the query that generates the
value.
This statement does not indicate any scope. Is it a serverwide value? Could a server running many scripts or many instances of one script return a value for mysql_insert_id() which was not generated by the last insert performed by the script calling it?
The scope of mysql_insert_id() is the MySQL connexion. Not the user + password, but the actual connection for the current script. (Note that the MySQL connection can also be a parameter of mysql_insert_id)
If you close and re-open the MySQL connexion, mysql_insert_id() will not return the id of the id inserted in the previous one.
If an id has been inserted just after your adding, but by another script execution (I mean with another connexion) the mysql_insert_id() will return your ID, not the ID actually created after your but in another connection.
Example :
$c1 = mysql_connect($srv, $usr, $pwd);
$c2 = mysql_connect($srv, $usr, $pwd);
$sql = "INSERT INTO table1 (col1, col2) VALUES('x', 'y') "
mysql_query($sql, $c1); // first insert
mysql_query($sql, $c2); // second insert
$id1 = mysql_insert_id($c1);
$id2 = mysql_insert_id($c2);
$id1 will be the id inserted first, $id2 the id inserted after.
Yes, please see the following article: How to Get the Unique ID for the Last Inserted Row
For LAST_INSERT_ID(), the most recently generated ID is maintained in
the server on a per-connection basis. It is not changed by another
client. It is not even changed if you update another AUTO_INCREMENT
column with a nonmagic value (that is, a value that is not NULL and
not 0). Using LAST_INSERT_ID() and AUTO_INCREMENT columns
simultaneously from multiple clients is perfectly valid. Each client
will receive the last inserted ID for the last statement that client
executed.
The scope is per-connection. If each run of the script opens a separate connection (which is what usually happens), then they will have separate scopes.
I'm currently using mysql_pconnect.
Is there a risk of erroneously retrieving the last id inserted?
Yes, there is a risk not only with last_insert_id but with transactions and other things.
mysql_pconnect isn't right thing for use it on production because many php instances will have access to a single connection
It seems, according to this note, that there might be a risk, when the insert query failed (quoting) :
be careful when using
last_insert_id() with persistent
connections - running
last_insert_id() after a failed
update/insert/etc will return the last
insert id of the last successful
update/insert made by that CONNECTION
rather than 0 for the number of rows
updated by the previous non-working
query