MySQL prepared statement with bulk insert - php

I have a module which allows admins to add some users (maybe multiple) to a group.
PHP
$user_id = $_POST["user_id"]; //this can be an array
$group_id = $_POST["group_id"];
$sql = "INSERT IGNORE INTO users_groups (user_id, group_id)
VALUES ($user_id[0],$group_id)";
for ( $i = 1; $i < count($user_id); $i++)
{
$sql .= ", ($user_id[$i],$group_id)"
}
As you can see the sql-query depends on how much users the admin has selected.
How can I use this code with prepared statements because the post-variables come from a user (SQL injections...)
UPDATE
I have two selectboxes:
one for the user selection (multiple selection is possible -> array): $_POST["user_id"]
one for the group selection (only one selection is possible): $_POST["group_id"]
And now I want a prepared SQL statement for inserting user_id and group_id to the many-to-many-table (users_groups). The problem is that the number of values which have to be inserted can change (depending how much users the admin has selected in the selectbox).
I want to change the prepared query depending on how much users the admin has selected.
For example:
the admin selected two users -> sql: INSERT IGNORE INTO users_groups (user_id, group_id) VALUES ($user_id[0],$group_id), ($user_id[1],$group_id)
the admin selected four users -> sql: INSERT IGNORE INTO users_groups (user_id, group_id) VALUES ($user_id[0],$group_id), ($user_id[1],$group_id), ($user_id[2],$group_id), ($user_id[3],$group_id)
My question: How can I do this automaticaly and with prepared sql statements because I dont want to have like 10 times if(count($user_id) == number) {...?
UPDATE 2
If I would do this manually the code would look like this:
$sql = $db->prepare("INSERT IGNORE INTO users_groups (user_id, group_id) VALUES (?, ?)");
$sql->bind_param('ii', $user_id[0], $group_id);
UPDATE 3
To check whether there are only integers in the post variables:
$user_id = filter_input_array(INPUT_POST, 'user_id', FILTER_SANITIZE_NUMBER_INT);
$user_id = abs($user_id);
$group_id = filter_input(INPUT_POST, 'group_id', FILTER_SANITIZE_NUMBER_INT);
$group_id = abs($group_id);

To create the SQL statement, you can use this to generate as many placeholders as you will need for all of your user_id values.
$user_ids = $_POST["user_id"]; //this can be an array
$group_id = $_POST["group_id"];
$placeholders = implode(', ', array_fill(0, count($user_ids), "(?, ?)"));
$sql = "INSERT IGNORE INTO users_groups (user_id, group_id) VALUES $placeholders";
Then to bind the values, if you are using pdo, something like this should work:
$stmt = $pdo->prepare($sql);
$i = 1;
foreach ($user_ids as $user_id) {
$stmt->bindValue($i++, $user_id);
$stmt->bindValue($i++, $group_id);
}
Or if you are using mysqli, you could try this approach using reflection (taken from a user note in the php docs here) the note does state that this needs PHP 5.3+, which hopefully you do have.
$stmt = $mysqli->prepare($sql);
$values[] = str_repeat('ii', count($user_ids));
foreach ($user_ids as $user_id) {
$values[] = $user_id;
$values[] = $group_id;
}
$ref = new ReflectionClass('mysqli_stmt');
$method = $ref->getMethod("bind_param");
$method->invokeArgs($stmt, $values);
$stmt->execute();
Or, because I'm stubborn and the thing from the php docs user note doesn't seem to work, which after reading more I don't see how it could work, considering the php doc for ReflectionMethod::invokeArgs specifically says
Note: If the function has arguments that need to be references, then they must be references in the passed argument list.
then using call_user_func_array could possibly work.
$stmt = $mysqli->prepare($sql);
$type = str_repeat('ii', count($user_ids));
foreach ($user_ids as $user_id) {
$values[] = $user_id;
$values[] = $group_id;
}
foreach ($values as $key => $value) {
$value_references[$key] = &$values[$key];
}
call_user_func_array('mysqli_stmt_bind_param',
array_merge(array($stmt, $type), $value_references));
But what a pain. I really like pdo.

Related

How to update two values in 100 existing rows in mysql with php most efficiently? [duplicate]

I have an array like:
$postdata[1] = 'This';
$postdata[2] = 'That';
$postdata[3] = 'The other';
And I want to loop through the array and update all of the rows where ID corresponds to the array key. Like:
foreach ($postdata as $key => $value) {
if ($key == 1) {
$update = $db->query("UPDATE site_email_templates SET Content='$postdata[1]' WHERE ID = 1");
} else if ($key == 2) {
$update = $db->query("UPDATE site_email_templates SET Content='$postdata[2]' WHERE ID = 2");
} else if ($key == 3) {
$update = $db->query("UPDATE site_email_templates SET Content='$postdata[3]' WHERE ID = 3");
}
}
What would be the simplest way to do this, not particularly knowing how many array keys there are, and keeping it all in one query?
You need to use prepared statements in order to avoid errors and vulnerabilities of all sorts and also to get some minor performance gain
$stmt = $db->prepare("UPDATE site_email_templates SET Content=? WHERE ID = ?");
$stmt->bind_param("ss", $content, $id);
foreach ($postdata as $id => $content)
{
$stmt->execute();
}
Reference: How can I prevent SQL injection in PHP?
Note: My answer is based on the PDO driver which in many aspects is better than mysqli. If you need mysqli solution please check the other answer provided by #Your Common Sense
The code below is tested on real environment and served with prepared statement preventing SQL-injection:
$sql = "UPDATE `site_email_templates` SET `Content` = (:content) WHERE `Id` = (:id)";
$stmt = $dbConn->prepare($sql);
foreach ($postdata as $id => $content)
{
$stmt->execute([':id' => $id, ':content' => $content]);
}
For more details about SQL injection you can read more:
https://www.owasp.org/index.php/SQL_Injection
For maximal speed, IODKU can do all the updates in a single statement. Caution: You should not use this for updating if you don't know that the ids exist.
INSERT INTO t
(id, -- A PRIMARY or UNIQUE key
col1, col2) -- column(s) to change
VALUES
(111, 22, 33),
(222, 33, 44),
...
ON DUPLICATE KEY UPDATE
col1 = VALUES(col1),
col2 = VALUES(col2);
You must provide some way to "bind" or "escape" the values to avoid sql-injection.

PDO Generate placeholders for binding from array and single value for a multiple INSERT statement

I am trying to generate a single insert statement that will insert multiple rows. I have and array of values, which is what I am wanting to insert into a table that all use the same userkey.
I have tried using a named PDO parameter and binding to that, then passing in the role array during execute but that doesn't work. So I moved on to placeholders, but I can't get that to work either.
I call my function like addUsersRoles(1, [100,101,102]);
And looking at the generated SQL I get:
INSERT user_roles (userkey, roleid) VALUES (?,?),(?,?),(?,?)
Which I think is the correct format for inserting multiple records.
Based up on that, what I am trying to generate is:
INSERT user_roles (userkey, roleid) VALUES (1,100),(1,101),(1,102)
How can I combine the power of PDO's binding to a SQL statement in this manner?
public function addUsersRoles($userkey, $roles = []){
$in = str_repeat('?,', count($roles) - 1) . '?';
$base_user_sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$sql = $base_user_sql;
foreach ($roles as $role) {
//$sql .= "(:USERKEY, $in),"; // Didn't Work
$sql .= "($in),";
}
//Remove trailing comma
$sql = rtrim($sql, ',');
$db = static::getDB();
$stmt = $db->prepare($sql);
//$stmt->bindValue(':USERKEY', $userkey, PDO::PARAM_STR);
return $stmt->execute($roles);
}
You can use placeholders as well. Look at the following example:
public function addUsersRoles(string $userKey, array $roles = []): bool
{
$values = [];
$inputParameters = [':user_key' => $userKey];
foreach ($roles as $index => $role) {
$rolePlaceholder = ':roleid' . $index;
$values[] = sprintf('(:user_key, %s)', $rolePlaceholder);
$inputParameters[$rolePlaceholder] = $role;
}
$sql = 'INSERT INTO user_roles (user_key, roleid) VALUES ';
$sql .= implode(', ', $values);
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($inputParameters);
}
This code will generate a query like this:
INSERT INTO user_roles (user_key, roleid) VALUES (:user_key, :roleid0), (:user_key, :roleid1), (:user_key, :roleid2), (:user_key, :roleid3), (:user_key, :roleid4);
And the $inputParameters will be like this:
[
':user_key' => 'some user key',
':roleid0' => 1,
':roleid1' => 2,
]
You shouldn't use count($roles) when making $in. It's always just ?, ?. You just need the count of roles when repeating that for all the rows. You can use array_fill to create an array of (?, ?) strings, and then implode to put commas between them.
You also need to insert create an array with alternating keys and roles, and use that as the parameters when executing.
public function addUsersRoles($userkey, $roles = []){
$values = implode(',', array_fill(0, count($roles), '(?, ?)'));
$base_user_sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$sql = $base_user_sql . $values;
$keys_and_roles = [];
foreach ($roles as $role) {
$keys_and_roles[] = $userkey;
$keys_and_roles[] = $role;
}
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($keys_and_roles);
}
I've simplified it a bit, but this splits into two parts that are done together. The first is to generate the SQL...
INSERT user_roles (userkey, roleid) VALUES (?,?),(?,?),(?,?)
This code just loops through the roles and adds (?,?), for each one.
The second part is building up the bind data. As the SQL needs a list of the data in the order userkey, roleid pairs, as it's building the SQL, it also adds these values to a data array at the same time.
So the main code comes out as...
public function addUsersRoles($userkey, $roles = []){
$sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$binds = [];
foreach ( $roles as $role ) {
$sql .= "(?,?),";
$binds[] = $userkey;
$binds[] = $role;
}
//Remove trailing comma
$sql = rtrim($sql, ',');
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($binds)
}
(Although I haven't been able to test the execute part).
You should also make sure any errors are being dealt with as well.

Insert multple rows with one insert

So I have three tables in a MySQL database:
order(id, etc..),
product(id, title, etc..)
orderproduct(productFK, orderFK)
Now I want to be able to insert an order with one order-id and (in some cases) multiple product-ids for orders containing more than one product:
order 1: orderid = 1, productids = 1
order 2: orderid = 2, productids = 2, 3
this while using prepared statements, like:
$stmt = $mysqli->prepare("INSERT INTO orderproduct (orderFK, productFK)
VALUES (?, ?)");
$result = $stmt->bind_param('ss', $orderid, $productid);
if($stmt->execute() == false) {
$flag = false;
}
$stmt->close();
One obvious solution is to loop the insert query but is there another way to do this without having to call the database multiple times?
This is working (hardcoded) but still, I can't figure out how to fill the bind_param dynamically..
$strings = "";
$values = "";
foreach ($params['products'] as $product) {
$strings .= 'ss';
$values .= "(?, ?),";
}
$values = substr($values, 0, -1);
$productid = array(1, 2);
$stmt = $mysqli->prepare("INSERT INTO orderproduct (orderFK, productFK)
VALUES " . $values);
$result = $stmt->bind_param($strings, $orderid, $productid[0], $orderid, $productid[1]);
if($stmt->execute() == false) {
$flag = false;
}
$stmt->close();
If you are on php 5.6+ you can use argument unpacking ... to bind your variables:
$args = [
$arg1,
$arg2,
$arg3,
$arg4,
];
$result = $stmt->bind_param($strings, ...$args);
An alternative would be to use PDO where you can send an array of arguments to bind to the execute() method.
Mysql will happily process something like:
INSERT INTO mytable (col1, col2)
VALUES (c1a, c2a)
,(C1b, c2b)
,(C1c, c2c)
...
(Up to max_allowed_packet) however expressing the bind_param() call cleanly (in a way which is easy to debug) will be rather difficult. Invoking with call_user_func_array() means you can simply pass a single array as the argument, but you still need to map your 2 dimensional data set to a one dimensional array.
Previously I've used the procedural mysql api to do multiple inserts resulting in a significant speed up of inserts and better balanced indexes.

Safe Way To Loop PDO Statement

Question:
Loop through a prepared PDO statement, check for duplicates, if no duplicates execute the query?
Current PDO Statement:
$STH = $DBH->prepare("INSERT INTO inbox (id,efrom,subject,msg,eread,date) VALUES ('',:efrom,:esubject,:emsg,:eread,:edate)");
$STH->bindParam(':efrom', $inbox_from[0]);
$STH->bindParam(':esubject', $inbox_subject[0]);
$STH->bindParam(':emsg', $inbox_msg[0]);
$STH->bindParam(':eread', $inbox_read[0]);
$STH->bindParam(':edate', $inbox_date[0]);
Why there needs to be a loop:
I need the arrays $inbox_* to be incremented then queried until it gets to the end of the array.
Example:
$inbox_from('hello','how','are','you');
someloop(somecondition) {
//output would be:
$STH->bindParam(':efrom', $inbox_from[0]);
$STH->bindParam(':efrom', $inbox_from[1]);
$STH->bindParam(':efrom', $inbox_from[2]);
$STH->bindParam(':efrom', $inbox_from[3]);
//It ends at [3] index because its the end of the array.
}
$STH = execute();
//So now it executes and should have put the 4 array indexes into different efrom columns. So column id 1 has 'hello' and id 4 has 'you'.
Maybe:
for($i = 0; //Something to check if array ended; ++$i) {
//Someway to Properly bind the arrays and execute?
}
or some use of a While Loop?
Hope I explained it my best.
Whats the best practice to use here?
Use numbered parameters instead of named parameters, and build the query and parameters dynamically.
$sql = "INSERT INTO inbox (efrom,subject,msg,eread,date) VALUES ";
// array_fill will create an array of N "(?, ?, ?, ?, ?)" strings
// implode will then join them together with comma separators
$sql .= implode(', ', array_fill(0, count($inbox_from), "(?, ?, ?, ?, ?)"));
$STH = $DBH->prepare($sql);
$params = array();
// Populate the $params array with all the input values
foreach ($inbox_from as $i => $from) {
$params[] = $from;
$params[] = $inbox_subject[$i];
$params[] = $inbox_msg[$i];
$params[] = $inbox_read[$i];
$params[] = $inbox_date[$i];
}
$STH->execute($params);
You can leave the id field out of the column list, and it will be filled in automatically using auto-increment.
To remove duplicate messages, you can do:
$check_stmt = $DBH->prepare("SELECT COUNT(*) AS count FROM inbox WHERE msg = :msg");
$check_stmt->bindParam(':msg', $msg);
$messages_seen = array();
foreach ($inbox_msg as $i => $msg) {
// Check if the message is already in the DB
$check_stmt->execute();
$first_row = $check_stmt->fetch(PDO::FETCH_OBJ);
$check_stmt->fetchAll(); // Fetch the rest of the query to get in sync
if ($first_row->count > 0) {
$messages_seen[$msg] = true; // Remember that we already saw this message
} elseif (!isset($messages_seen[$msg])) // If we haven't already seen this message
$params[] = $inbox_from[$i];
$params[] = $inbox_subject[$i];
$params[] = $msg;
$params[] = $inbox_read[$i];
$params[] = $inbox_date[$i];
$messages_seen[$msg] = true; // Remember that we added this message
}
}
$sql = "INSERT INTO inbox (efrom,subject,msg,eread,date) VALUES ";
// There's 1 (...) group for every 5 parameters, so divide the length of $params by 5 to know how many of them to put in the SQL
$sql .= implode(', ', array_fill(0, count($params)/5, "(?, ?, ?, ?, ?)"));
$STH = $DBH->prepare($sql);
$STH->execute($params);
When adding an index on a TEXT datatype, you have to specify the number of bytes of the text to store in the index. So it should be something like:
CREATE INDEX ix_msg ON inbox (msg(200));
You can create one big query if you are using Insert to same table
INSERT INTO table (columns) VALUES (....), (....)...., (...)
Its better than calling to sql for each row. Faster too

MySQLi query to loop through array and update multiple rows

I have an array like:
$postdata[1] = 'This';
$postdata[2] = 'That';
$postdata[3] = 'The other';
And I want to loop through the array and update all of the rows where ID corresponds to the array key. Like:
foreach ($postdata as $key => $value) {
if ($key == 1) {
$update = $db->query("UPDATE site_email_templates SET Content='$postdata[1]' WHERE ID = 1");
} else if ($key == 2) {
$update = $db->query("UPDATE site_email_templates SET Content='$postdata[2]' WHERE ID = 2");
} else if ($key == 3) {
$update = $db->query("UPDATE site_email_templates SET Content='$postdata[3]' WHERE ID = 3");
}
}
What would be the simplest way to do this, not particularly knowing how many array keys there are, and keeping it all in one query?
You need to use prepared statements in order to avoid errors and vulnerabilities of all sorts and also to get some minor performance gain
$stmt = $db->prepare("UPDATE site_email_templates SET Content=? WHERE ID = ?");
$stmt->bind_param("ss", $content, $id);
foreach ($postdata as $id => $content)
{
$stmt->execute();
}
Reference: How can I prevent SQL injection in PHP?
Note: My answer is based on the PDO driver which in many aspects is better than mysqli. If you need mysqli solution please check the other answer provided by #Your Common Sense
The code below is tested on real environment and served with prepared statement preventing SQL-injection:
$sql = "UPDATE `site_email_templates` SET `Content` = (:content) WHERE `Id` = (:id)";
$stmt = $dbConn->prepare($sql);
foreach ($postdata as $id => $content)
{
$stmt->execute([':id' => $id, ':content' => $content]);
}
For more details about SQL injection you can read more:
https://www.owasp.org/index.php/SQL_Injection
For maximal speed, IODKU can do all the updates in a single statement. Caution: You should not use this for updating if you don't know that the ids exist.
INSERT INTO t
(id, -- A PRIMARY or UNIQUE key
col1, col2) -- column(s) to change
VALUES
(111, 22, 33),
(222, 33, 44),
...
ON DUPLICATE KEY UPDATE
col1 = VALUES(col1),
col2 = VALUES(col2);
You must provide some way to "bind" or "escape" the values to avoid sql-injection.

Categories