Alternative to "PDO::lastInsertId" / "mysql_insert_id" - php

I always hear that using "lastInsertId" (or mysql_insert_id() if you're not using PDO) is evil. In case of triggers it obviously is, because it could return something that's totally not the last ID that your INSERT created.
$DB->exec("INSERT INTO example (column1) VALUES ('test')");
// Usually returns your newly created ID.
// However when a TRIGGER inserts into another table with auto-increment:
// -> Returns newly created ID of trigger's INSERT
$id = $DB->lastInsertId();
What's the alternative?

IMHO it's only considered "evil" because hardly any other SQL database (if any) has it.
Personally I find it incredibly useful, and wish that I didn't have to resort to other more complicated methods on other systems.

One alternative is to use sequences instead, so you generate the ID yourself before you do the insert.
Unfortunately they are not supported in MySQL but libraries like Adodb can emulate them using another table. I think however, that the emulation itself will use lastInsertId() or equivalent... but at least you are less likely to have a trigger on a table which is purely used for a sequence

If you go the route of ADOdb (http://adodb.sourceforge.net/), then you can create the insert ID before hand and explicitly specific the ID when inserting. This can be implemented portably (ADOdb supports a ton of different databases...) and guarantees you're using the correct insert ID.
The PostgreSQL SERIAL data type is similar except that it's per-table/per-sequence, you specify the table/sequence you want the last insert ID for when you request it.

I guess it's not really state of the art either but I use write locks to make sure that I really get the last inserted ID.

It's not sophisticated and it's not efficient, but if the data you've inserted include unique fields, then a SELECT can obviously yield what you're after.
For example:
INSERT INTO example (column1) VALUES ('test');
SELECT id FROM example WHERE column1 = 'test';

You could try this:
$sql = "SELECT id FROM files ORDER BY id DESC LIMIT 1";
$PS = $DB -> prepare($sql);
$PS -> execute();
$result = $PS -> fetch();

Related

How can I use an SQL query's result for the WHERE clause of another query?

Okay, basically I have a table that contains statements like:
incident.client_category = 1
incident.client_category = 8
incident.severity = 1
etc.
I would like to use the contents from this table to generate other tables that fulfill the conditions expressed in this one. So I would need to make it something like
SELECT * FROM incident WHERE incident.client_category = 1
But the last part of the where has to come from the first table. Right now what I'm trying to do is something like
SELECT * FROM incident WHERE (SELECT condition FROM condition WHERE id = 1)
id = 1 stands for the condition's id. Right now I only want to work with ONE condition for testing purposes. Is there a way to achieve this? Because if there isn't, I might have to just parse the first query's results through PHP into my incident query.
Table schemas:
Engineering Suggestion - Normalize the DB
Storing a WHERE clause, like id = 10, in a field in a MySQL table, is not a good idea. I recommend taking a look at MySQL Normalization. You shouldn't store id = 10 as a varchar, but rather, you should store something like OtherTableid. This allows you to use indices, to optimize your DB, and to get a ton of other features that you are deprived of by using fields as WHERE clauses.
But sometimes we need a solution asap, and we can't re-engineer everything! So let's take a look at making one...
Solution
Here is a solution that will work even on very old, v. 5.0 versions of MySQL. Set the variable using SET, prepare a statement using PREPARE, and execute it using EXECUTE. Let's set our query into a variable...
SET #query = CONCAT(
"SELECT * FROM incident WHERE ",
(SELECT condition FROM condition WHERE id = 1)
);
I know for a fact that this should work, because the following definitely works for me on my system (which doesn't require building any new tables or schema changes)...
SET #query = CONCAT("SELECT id FROM myTable WHERE id = ", (SELECT MAX(id) FROM myTable));
If I SELECT #query;, I get: SELECT id FROM myTable WHERE id = 1737901. Now, all we need to do is run this query!
PREPARE stmt1 FROM #query;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
Here we use a prepare to build the query, execute to execute it, and deallocate to be ready for the next prepared statement. On my own example above, which can be tested by anyone without DB schema changes, I got good, positive results: EXECUTE stmt1; gives me...
| id | 1737901 | .
here is one way to achieve your goal by using what is called dynamic sql, be ware that this works only select from condition table returns only one record.
declare #SQLSTRING varchar(4000)
, #condition VARCHAR(500) -- change the size to whatever condition column size is
SELECT #condition = condition
FROM
condition
WHERE
id = 1
SET #SQLSTRING= 'SELECT * FROM incident WHERE ' + #condition
exec sp_executesql(#SQLSTRING)
Since you have also tagged the question with PHP, I would suggest using that. Simply select the string from the condition table and use the result to build up a SQL query (as a string in PHP) including it. Then run the second query. Psudo-code (skipping over what library/framework you re using to call the db):
$query = "select condition from condition where id = :id";
$condition = callDbAndReturnString($query, $id);
$query = "select * from incident where " . $condition;
$result = callDb($query);
However, be very careful. Where and how are you populating the possible values in the condition table? Even how is your user choosing which one to use? You run the risk of opening yourself up to a secondary SQL injection attack if you allow the user to generate values and store them there. Since you are using the value from the condition table as a string, you cannot parametrise the query using it as you (hopefully!) normally would. Depending on the queries you run and the possible values there as conditions, there might also be risk even if you just let them pick from a pre-built list. I would seriously ask myself if this (saving parts of SQL queries as strings in another table) is the best approach. But, if you decide it is, this should work.

PDO rowcount used often

I often use the function rowCount of PDO like this for example:
$sql = $dataBase->prepare('SELECT email, firstname, lastname
FROM pr__user
WHERE id = :id');
$sql->execute(array('id' => $_SESSION['user_id']));
$rowCount = $sql->rowCount();
It al the time worked fine but I saw in the PHP manual:
If the last SQL statement executed by the associated PDOStatement was
a SELECT statement, some databases may return the number of rows
returned by that statement. However, this behaviour is not guaranteed
for all databases and should not be relied on for portable
applications.
http://php.net/manual/en/pdostatement.rowcount.php
It works fine with MySQL and MariaDB so I kept on using it. As I use it an application I wish portable, should I modify my code?
I never ask for the row count. Querying always returns an array of result rows (in some format), I can simply ask how many rows in the array -- such as with PHP's count(..) function.
What you're missing is that PDO is an interface to many different databases, not just MySQL. They make no guarantees that the function will return the same sort of values on completely different back-ends.
This is what "for portable applications" means: If you want your code to run universally on an arbitrary database you may need to avoid using that function. If that's not the case, you're not writing generic library code, you can depend on MySQL's particular behaviour.
Just be sure to test whatever you're doing to ensure that assumption is reasonable.
Rather, it's just pointless and superfluous. You are bloating your code for no reason.
From your example it is evident that you are going to use the data selected (otherwise there is no point in selecting email). So it means that you can use that data instead of row count all the way. Assuming the next operator would be fetch(), you can omit rowCount()
$sql = $dataBase->prepare('SELECT email, firstname, lastname
FROM pr__user
WHERE id = :id');
$sql->execute(array('id' => $_SESSION['user_id']));
$user = $sql->fetch();
if ($user) { ...
and from now on you can use $user in the every single condition where $rowCount has been used. Simply because you don't actually need a count here, but rather a boolean flag for which purpose an array would serve as good as an integer.
Even in a case when you don't need email but only to know whether a user exists, you can simply select just 1 scalar value and then fetch it - so your code remains uniform.

How does PDO's LastInsertId actually work?

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.

Which is the most secure and correct solution to display only one value from a single row using SELECT in PHP?

My question is which solution do you think is best to use when wanting to retrieve only one value from a MySQL DB row.
For example, let's say we have the following:
-table "users" with three rows "username","password" and "status" AND three users "user1","user2" and "user3".
If I want to select only the status of one user (let's say user1) and set it to a variable, I will use:
$user_status = mysql_result(mysql_query("SELECT status FROM users WHERE username='user1'"),0);
I searched the net and I see that people use different methods of retrieving this type of info, such as setting LIMIT 1 inside the select code or by retrieving the whole users list and then sort the one that matches their needs.
I am wondering if my solution is the best and secure way (including security from SQL inject, keeping in mind that no $_GET method is used in the php code).
Maybe use both LIMIT 1 and the method I used above (for the code to require less resources and time to execute)?
From a database point of view the safest way is to have a unique key in the table you are selection from and retrieve the row via this key. In your example you could have a userID column that holds a unique ID for each user. If you query WHERE userID='...' the database guarantees you that there can only be one result row.
Edit: "Public opinion" suggested that I add two things.
Thou shall not use mysql_*! Why not use mysqli? You should not have to worry about its performance.
There is no reason to use LIMIT 1 if you are using proper database design - no reason at all. Its a bit like writing code that says Enter car; Make sure you have entered exactly one car;. LIMIT can be used in other cases like retrieving the first 10 results of many.
you should use the PDO methods if you want any security, regular mysql calls have been fased out for a while
Use mysqli (http://ee1.php.net/mysqli), but check here http://www.php.net/manual/en/book.pdo.php
Your query though would be SELECT status FROM users WHERE username='user1' LIMIT 1
First one - you don't use mysql_ methods anymore - they're deprecated and natively unsafe.
Second - you use PDO interface, like this
$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';
try {
$dbh = new PDO($dsn, $user, $password);
$stmt = $dbh->prepare("SELECT `status` FROM `users` WHERE `username` = ?");
$stmt->execute(array($_GET['username']));
$status = $stmt->fetchColumn();
} catch (PDOException $e) {
echo 'PDO Error : ' . $e->getMessage();
}
Using prepare - execute chain is safe with PDO - it is automatically sanitized, so you don't have to worry about mysql injections. At prepare part you create a query with parameter, and in execute part you execute your prepared query with parameter equaling $_GET and store the result. Any error you encounter along the way is being caught in the catch (PDOException $e) block and can be handled appropriately.

Postgresql and PHP: is the currval a efficent way to retrieve the last row inserted id, in a multiuser application?

Im wondering if the way i use to retrieve the id of the last row inserted in a postgresql table is efficent..
It works, obviously, but referencing on the serial sequence currval value could be problematic when i have many users adding rows in the same table at the same time.
My actual way is:
$pgConnection = pg_connect('host=127.0.0.1 dbname=test user=myuser password=xxxxx')or die('cant connect');
$insert = pg_query("INSERT INTO customer (name) VALUES ('blabla')");
$last_id_query = pg_query("SELECT currval('customer_id_seq')");
$last_id_results = pg_fetch_assoc($last_id_query);
print_r($last_id_results);
pg_close($pgConnection);
Well, its just a test atm.
But anyway, i can see 3 issues with this way:
Referencing on the customer_id_seq, if two user do the same thing in the same time, could happen that them both get the same id from that way... or not?
I have to know the table's sequence name. Becose pg_get_serial_sequence dont works for me (im newbie on postgresql, probably is a configuration issue)
Any suggestion/better ways?
p.s: i can't use the PDO, becose seem lack a bit with the transaction savepoint; I wont use zend and, in the end, i'll prefer to use the php pg_* functions (maybe i'll build up my classes in the end)
EDIT:
#SpliFF(thet deleted his answer): this would works better?
$pgConnection = pg_connect('host=127.0.0.1 dbname=test user=myuser password=xxxxx')or die('cant connect');
pg_query("BEGIN");
$insert = pg_query("INSERT INTO customer (name) VALUES ('blabla')");
$last_id_query = pg_query("SELECT currval('customer_id_seq')");
$last_id_results = pg_fetch_assoc($last_id_query);
print_r($last_id_results);
//do somethings with the new customer id
pg_query("COMMIT");
pg_close($pgConnection);
If you use a newer version of PostgreSQL (> 8.1) you should use the RETURNING clause of INSERT (and UPDATE) command.
OTOH if you insist on using one of the sequence manipulation functions, please read the fine manual. A pointer: "Notice that because this is returning a session-local value, it gives a predictable answer whether or not other sessions have executed nextval since the current session did."
Insert and check curval(seq) inside one transaction. Before commiting transaction you'll see curval(seq) for your query and no matter who else inserted at the same time.
Don't remember the syntax exactly - read in manual (last used pgsql about 3 years ago), but in common it looks like this:
BEGIN TRANSACTION;
INSERT ...;
SELECT curval(seq);
COMMIT;
ex. minsert into log (desc,user_id) values ('drop her mind',6) returning id

Categories