I'm doing some queries into MySQL database using MySQLi extension in PHP. I need to do a query like this:
SELECT col1, col2 FROM mytable WHERE id IN (2, 4, 6);
This is a concrete example of the query, but the number of ids inside the parenthesis after the IN keyword will vary. Since I want to use the mysqli->prepare() function (prepared statement) I would do it like this:
$statement->prepare("SELECT col1, col2 FROM mytable WHERE id IN (?)");
$statement->bind_pararm("s".... something)
I'm not sure what to put in the bind param function, since i cant know how many IDs will actually be queried for. I couldn't find any examples how to use IN keyword inside prepared statements.
If you prepare your statement once you need it, you could create the placeholders like
join(",", array_fill(0, count($ids) , "?"))
or something.
Related
Alright so I'll try to explain it as simple as possible; consider that I have two database tables (MySQL Server / MariaDB, database-related tasks coded in procedural style in PHP using prepared statements):
in one, I have a column of datatype JSON, whose content corresponds to sth like {name1:info,name2:info}
In another one, I have simple non-json records, having a structure like:
name | status
------+--------
name1 | statusX
------+--------
name2 | statusY
My Goal: I need to retrieve the name2 from table 1), but I also need to retrieve the status of the person having that same name (which in this case is statusY). Note that, for the retrieval of name2, I cannot rely on indexes of the json object (name2 may be the first key of the json object).
How I would do it so far:
A) Get the name2 from table 1) in a first query, sanitize it, and
B) use it in the second query which then correctly retrieves the statusY
Both statements A) and B) are parametrized prepared sql statements, triggered by an AJAX Call at a regular interval (AJAX Polling).
Given that these database queries are thus executed frequently, I want them to be executed as fast as possible, and thus ideally reduce my two queries above to a single one. My problem: I need the result of statement A) to execute statement B), so I cannot summarize the two queries into a single prepared statement, as prepared statements cannot contain multiple sql statements. The best solution to reach what I want is create a stored procedure like:
SET #name = SELECT ..... FROM table_1; SELECT .... FROM table_2;
And then execute it as parametrized prepared statement; is that correct?
I'm not at all experienced with stored procedures in MySQL Server, didn't really need them yet, but they seem to be the only solution if you want to wrap > 1 sql statements into a single prepared statement. Is this assumption, and thus the conclusion that I gotta create a stored procedure to reach what I want, correct?
IMPORTANT NOTE: I don't know the name I need to query. From the two names of the json column of table 1), I only know the other name. In other words, I have one name of a person X, and I want to get the status of all the persons which have been associated with that person X in table 1), while the status of each person is listed in table 2), to avoid to have duplicate status storage in the DB. ATM, I retrieve the other names of each relation record from DB 1) by using a conditional statement saying sth like
UPDATE
See added answer below, got it working.
You can query JSON data type with MySQL (if version > 5.7), and thus you can simply do everything with a single query
Give this a try
SELECT t1.name1, t1.name2, t2.status
FROM
(
SELECT JSON_EXTRACT(your_json_column, "$.name1") AS name1,
JSON_EXTRACT(your_json_column, "$.name2") AS name2
FROM table1
WHERE JSON_EXTRACT(your_json_column, "$.name1") = 'info'
) t1
INNER JOIN table2 t2 ON t2.`name`=t1.name2
Adapt the name of your_json_column. Also I assumed that you wanted to search the name2 of a specific name1, thus my WHERE clause, remove it if it was a false assumption.
Okay got it working, pretty much thanks to the solution proposed by Thomas G and some hints of JNevill (cheers guys!):
SELECT t1.info1, t1.info2, t1.info3, t1.other_name, t2.status FROM (
SELECT
field1 AS info1,
field2 AS info2,
field3 AS info3,
CASE
WHEN JSON_VALUE(JSON_KEYS(json_names_column),"$[0]") = 'name1'
THEN JSON_VALUE(JSON_KEYS(json_names_column),"$[1]")
ELSE JSON_VALUE(JSON_KEYS(json_names_column),"$[0]")
END
AS other_name
FROM table1
WHERE id = 345
) t1 INNER JOIN table2 t2 ON t1.other_name = t2.name;
Note that I used JSON_VALUE(JSON_KEYS()) instead of JSON_EXTRACT, to only return the needed name as name data of t1, and because I don't know the name to retrieve before the query, so I cannot use the WHEREclause proposed by Thomas G.
Simple question - I do understand that if I want to run this type of query in Wordpress:
SELECT * FROM tableA WHERE variable1 = $var1
...then I need to use 'prepare', like so:
$my_query = $wpdb->get_results($wpdb->prepare("SELECT * FROM tableA
WHERE variable1 = %s", $var1) );
However, when I want to do this type of query instead:
join two tables on a column
and NOT use a variable
like this:
SELECT * FROM tableA, tableB WHERE tableA.some_col = tableB.some_other_col
...'should' I:
still use some form of 'prepare' statement to safeguard against SQL injection
or is it ok to do the following:
$my_query = $wpdb->get_results("SELECT * FROM tableA, tableB WHERE
tableB.some_col = tableB.some_other_col");
You dont need prepared statements there because there is no way that anyone could use this code for sql injection.
If you just select everything from two tables without giving any variable you can forget about prepared statements :)
I think you should stick with
$wpdb->prepare
even if you do not have any un-sanitized parameters to pass to the query.
There are few reasons why you should do that:
This is a best practice;
Consistency. If you will use the same approach for all your queries;
If you will need to pass a parameter to this query in future, it will be easier for you.
You find a discussion about this here Should I use wpdb prepare?
I am adding a temporary table which contains a list of filenames which a second query will use. I understand that filenames can be used for sql injection, so I want to use prepared statements.
A simplified version of my working query looks like this (e.g. there could be 50 filenames):
CREATE TEMPORARY TABLE tempfile(filename varchar(100));
INSERT INTO tempfile (filename)
SELECT filename FROM (
SELECT 'myfile.jpg' AS filename UNION SELECT "myfile2.jpg") xx;
I tried to replace it with:
CREATE TEMPORARY TABLE tempfile(filename varchar(100));
INSERT INTO tempfile (filename)
SELECT filename FROM (
SELECT ? AS filename UNION SELECT ?) xx;
But mysql gives me a syntax error. Any ideas how to use parameters with this query?
And if you can give me a better title for this post, I will change it.
UPDATE - NOTE
The above query will insert two records. You can then query them with:
SELECT tf.filename FROM tempfile
Try something like this:
INSERT INTO tempfile (filename)
VALUES
(?),(?),(?);
I was converting a multi_query to a prepared statement and each statement needed to be run with a separate prepare and execute. Simple and obvious.
In essence, I have a value that I have to call a couple times in my SQL query. Thus, is it possible to reuse the same named placeholder in the statement e.g.
SELECT :Param FROM Table WHERE Column = :Param, then simply bindValue(":Param"), and have the value be there for both :Params?
PDO::prepare states that "you cannot use a named parameter marker of the same name twice in a prepared statement", so I guess that's a no then.
You can if you set PDO::ATTR_EMULATE_PREPARES = true.
E.g. $connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);.
If you're using Laravel you can set this in an options array in config/database.php. e.g. PDO::ATTR_EMULATE_PREPARES => true
Apart from reuse, the main issue here is that you are trying to dynamically change col names.
This answer posted by an anonymous user on http://php.net/manual/en/pdo.prepare.php :
To those wondering why adding quotes to around a placeholder is wrong,
and why you can't use placeholders for table or column names:
There is a common misconception about how the placeholders in prepared
statements work: they are not simply substituted in as (escaped)
strings, and the resulting SQL executed. Instead, a DBMS asked to
"prepare" a statement comes up with a complete query plan for how it
would execute that query, including which tables and indexes it would
use, which will be the same regardless of how you fill in the
placeholders.
The plan for "SELECT name FROM my_table WHERE id = :value" will be the
same whatever you substitute for ":value", but the seemingly similar
"SELECT name FROM :table WHERE id = :value" cannot be planned, because
the DBMS has no idea what table you're actually going to select from.
Even when using "emulated prepares", PDO cannot let you use
placeholders anywhere, because it would have to work out what you
meant: does "Select :foo From some_table" mean ":foo" is going to be a
column reference, or a literal string?
When your query is using a dynamic column reference, you should be explicitly white-listing the columns you know to exist on the table, e.g. using a switch statement with an exception thrown in the default: clause.
Many queries like yours can be rewritten to use only one placeholder.
SELECT :Param FROM Table WHERE Column = :Param
would be the same as
SELECT Column FROM Table WHERE Column = :Param
But sometimes it's not that simple. For example:
SELECT *
FROM my_table
WHERE first_name LIKE :Param
OR last_name LIKE :Param
OR biography LIKE :Param
In such case you could reuse the parameter value storing it in a cross joined derived table (subquery in FROM clause):
SELECT t.*
FROM my_table t
CROSS JOIN (SELECT :Param as Param) AS x
WHERE first_name LIKE x.Param
OR last_name LIKE x.Param
OR biography LIKE x.Param
There's a workaround:
function search($criteria) {
$sql = "SELECT * FROM my_table
WHERE column_1 like CONCAT('%', :criteria1, '%')
OR column_2 like CONCAT('%', :criteria2, '%')
OR column_3 like CONCAT('%', :criteria3, '%')
";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':criteria1', $criteria);
$stmt->bindParam(':criteria2', $criteria);
$stmt->bindParam(':criteria3', $criteria);
$stmt->execute();
return($stmt->fetchAll(PDO::FETCH_ASSOC));
}
In summary, use different placeholders with the same criteria.
In essence, I have a value that I have to call a couple times in my SQL query. Thus, is it possible to reuse the same named placeholder in the statement e.g.
SELECT :Param FROM Table WHERE Column = :Param, then simply bindValue(":Param"), and have the value be there for both :Params?
PDO::prepare states that "you cannot use a named parameter marker of the same name twice in a prepared statement", so I guess that's a no then.
You can if you set PDO::ATTR_EMULATE_PREPARES = true.
E.g. $connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);.
If you're using Laravel you can set this in an options array in config/database.php. e.g. PDO::ATTR_EMULATE_PREPARES => true
Apart from reuse, the main issue here is that you are trying to dynamically change col names.
This answer posted by an anonymous user on http://php.net/manual/en/pdo.prepare.php :
To those wondering why adding quotes to around a placeholder is wrong,
and why you can't use placeholders for table or column names:
There is a common misconception about how the placeholders in prepared
statements work: they are not simply substituted in as (escaped)
strings, and the resulting SQL executed. Instead, a DBMS asked to
"prepare" a statement comes up with a complete query plan for how it
would execute that query, including which tables and indexes it would
use, which will be the same regardless of how you fill in the
placeholders.
The plan for "SELECT name FROM my_table WHERE id = :value" will be the
same whatever you substitute for ":value", but the seemingly similar
"SELECT name FROM :table WHERE id = :value" cannot be planned, because
the DBMS has no idea what table you're actually going to select from.
Even when using "emulated prepares", PDO cannot let you use
placeholders anywhere, because it would have to work out what you
meant: does "Select :foo From some_table" mean ":foo" is going to be a
column reference, or a literal string?
When your query is using a dynamic column reference, you should be explicitly white-listing the columns you know to exist on the table, e.g. using a switch statement with an exception thrown in the default: clause.
Many queries like yours can be rewritten to use only one placeholder.
SELECT :Param FROM Table WHERE Column = :Param
would be the same as
SELECT Column FROM Table WHERE Column = :Param
But sometimes it's not that simple. For example:
SELECT *
FROM my_table
WHERE first_name LIKE :Param
OR last_name LIKE :Param
OR biography LIKE :Param
In such case you could reuse the parameter value storing it in a cross joined derived table (subquery in FROM clause):
SELECT t.*
FROM my_table t
CROSS JOIN (SELECT :Param as Param) AS x
WHERE first_name LIKE x.Param
OR last_name LIKE x.Param
OR biography LIKE x.Param
There's a workaround:
function search($criteria) {
$sql = "SELECT * FROM my_table
WHERE column_1 like CONCAT('%', :criteria1, '%')
OR column_2 like CONCAT('%', :criteria2, '%')
OR column_3 like CONCAT('%', :criteria3, '%')
";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':criteria1', $criteria);
$stmt->bindParam(':criteria2', $criteria);
$stmt->bindParam(':criteria3', $criteria);
$stmt->execute();
return($stmt->fetchAll(PDO::FETCH_ASSOC));
}
In summary, use different placeholders with the same criteria.