PDOStatement->prepare and PDOStatement->bindParam() combination not working [duplicate] - php

This question already has answers here:
Can PHP PDO Statements accept the table or column name as parameter?
(8 answers)
Closed 8 years ago.
I have some code that should loop through values and change entries in a table. The 5 values of the variables $change_val, $column, and $id all echo out correctly, so I assume there is something wrong with my usage of bindParam (but I am not sure what it is).
$connection = new PDO("mysql:host=localhost;dbname=logbook", $username, $password);
$perform_edit = $connection->prepare("UPDATE contacts SET :column = :value WHERE name_id = :name_id");
[Definition of Arrays]
for ($i = 1; $i <= 5; $i++) {
if (!empty($_POST[ $change_array[$i]])) {
$change_val = $_POST[$change_array[$i]];
$column = $column_array[$i];
$id = $_POST["name_id_ref"];
$perform_edit->bindParam(":column", $column, PDO::PARAM_STR);
$perform_edit->bindParam(":value", $_POST[$change_array[$i]], PDO::PARAM_STR);
$perform_edit->bindParam(":name_id", $_POST["name_id_ref"], PDO::PARAM_INT);
$perform_edit->execute();
}
}
The $_POST statement is there because the value I want is actually passed from another file. When I place appropriate echo statements within the loop, though, they all print out their correct value.
I've also tried bindValue, but that did not work either. I see no errors and things at least compile smoothly—just not as they should. Nothing in the table is changed.
What's wrong here?

You cannot use place holders for table or column names it would defeat the purpose of preparing a statement ahead of time if the structure of that statement changed.
You would need to pre-build your prepare statement with the correct column names, whether you name them by hand, string replacement, or implode a list of column names.
I don't have an environment to test on right now but something like:
//Some random values and DB column names
$arrLocation = array ('Victoria','Washington','Toronto','Halifax','Vancouver');
$arrName = array ('Sue', 'Bob', 'Marley', 'Tim', 'Fae');
$arrColumn = array (1 => 'name', 2 => 'age', 3 => 'location');
/* Build column & named placeholders
* $strSet = '`name` = :name, `age` = :age, `location` = :location';
*/
$strSet = '';
foreach ($arrColumn as $column) {
$strSet .= "`$column` = :$column, ";
}
$strSet = rtrim($strSet, ', ');
$connection = new PDO($dsn, $user, $pass);
/*
* Prepared statement then evaluates to:
* UPDATE `table` SET `name` = :name, `age` = :age, `location` = :location
* WHERE `id` = :id;
*/
$stmt = $connection->prepare("UPDATE `table` SET $strSet WHERE `id` = :id;");
$arrChange = array (
1 => $arrName[(rand(0, count($arrName)-1))],
2 => rand(0, 30),
3 => $arrLocation[(rand(0, count($arrLocation)-1))]
);
$idToUpdate = 1;
$stmt->bindParam(':id', $idToUpdate, PDO::PARAM_INT);
foreach($arrChange as $key=>$value) {
$stmt->bindValue(":$arrColumn[$key]", $value);
}
$stmt->execute();

Related

PDO sqlsrv: right trimming on LIKE on char fields

I noticed that if you open a connection with PDO :: SQLSRV_ATTR_ENCODING = PDO :: SQLSRV_ENCODING_UTF8 (default configuration) there is a problem when using LIKE with named parameters on char fields.
No automatic trim of the padding spaces is performed, an operation that is performed in all other cases.
How to reproduce the problem:
Create a table and insert data in it:
CREATE TABLE testDB.dbo.TEST_TABLE (
ID_FIELD int IDENTITY(1,1) NOT NULL,
CHAR_FIELD char(15) COLLATE Latin1_General_CI_AS DEFAULT ' ' NOT NULL
CONSTRAINT TEST_TABLEK00 PRIMARY KEY (ID_FIELD)
);
INSERT INTO TEST_TABLE (CHAR_FIELD) VALUES ('Test data'), ('MyString'), ('My data 123');
Then on PHP I get this results
$options = array();
$pdo = new PDO("sqlsrv:Server=testServer;Database=testDB", 'test', 'test', $options);
$stmt = $pdo->prepare("SELECT * FROM TEST_TABLE WHERE CHAR_FIELD = 'Test data'");
$stmt->execute();
$results = $stmt->fetchAll(); //Returns 1 row
$stmt = $pdo->prepare("SELECT * FROM TEST_TABLE WHERE CHAR_FIELD LIKE 'Test data'");
$stmt->execute();
$results = $stmt->fetchAll(); //Returns 1 row
$stmt = $pdo->prepare("SELECT * FROM TEST_TABLE WHERE CHAR_FIELD = :CHAR_FIELD");
$value = 'Test data';
$stmt->bindParam('CHAR_FIELD', $value, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(); //Returns 1 row
$stmt = $pdo->prepare("SELECT * FROM TEST_TABLE WHERE CHAR_FIELD LIKE :CHAR_FIELD");
$value = 'Test data';
$stmt->bindParam('CHAR_FIELD', $value, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(); //Returns 0 rows
$stmt = $pdo->prepare("SELECT * FROM TEST_TABLE WHERE CHAR_FIELD LIKE :CHAR_FIELD");
$value = 'Test data ';
$stmt->bindParam('CHAR_FIELD', $value, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(); //Returns 1 row
$options = array(PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_SYSTEM);
$pdo = new PDO("sqlsrv:Server=testServer;Database=testDB", 'test', 'test', $options);
$stmt = $pdo->prepare("SELECT * FROM TEST_TABLE WHERE CHAR_FIELD LIKE :CHAR_FIELD");
$value = 'Test data';
$stmt->bindParam('CHAR_FIELD', $value, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(); //Returns 1 row
The behavior of the like together with the named parameter, when opening the connection to the DB in UTF8, is not uniform with all the other behaviors.
Only in that case is the field not automatically trimmed while in all other cases it is. Personally, it seems to me more of a bug than a deliberate behavior.
I find myself managing a huge application, developed over 10 years and which can interface with different databases (mysql, postgres, oracle and sqlserver).
Changing the queries one by one is impossible, I would need a solution at the configuration level (some flags to set in the PDO or on SqlSever itself) or at least find a way to normalize this behavior to all the others automatically via code.

PDO Binding in foreach fails at prepare

I would like to create a query that may or may not have more than one part. This means, I will be including an array and looping through it, and appending a query to the main SQL query, then finally prepare it.
First of all, I have defined the sql query
$sql = '';
then, I defined a foreach looping value
$arrayLoopValue = 0;
after that, I have created a foreach loop. In which I increased the arrayLoopValue, appended the sql with a new query based on the array's index.
foreach($questionsArray as $questionAnswerRow){
$arrayLoopValue = $arrayLoopValue + 1;
$sql = $sql .
'INSERT INTO gosurveys_surveys_questions_answers
SET survey_id = :survey_id_' . $arrayLoopValue .
', question_id = :question_id_' . $arrayLoopValue .
', user_email = :user_email_' . $arrayLoopValue .
', answer_type = :answer_type_' . $arrayLoopValue .
', question_answer = :question_answer_' . $arrayLoopValue .
', question_answer_creation_date = UTC_TIMESTAMP(); ';
}
The database / example for this query is NOT important, as all fields match and it's already empty. Only the structure, which is provided above, is required.
This fails at the following line.
$query = $this->conn->prepare($sql);
I tried to echo the query and see if there's something wrong. I got the following output:
INSERT INTO gosurveys_surveys_questions_answers
SET survey_id = :survey_id_1,
question_id = :question_id_1,
user_email = :user_email_1,
answer_type = :answer_type_1,
question_answer = :question_answer_1,
question_answer_creation_date = UTC_TIMESTAMP();
INSERT INTO gosurveys_surveys_questions_answers
SET survey_id = :survey_id_2,
question_id = :question_id_2,
user_email = :user_email_2,
answer_type = :answer_type_2,
question_answer = :question_answer_2,
question_answer_creation_date = UTC_TIMESTAMP();
Which is correct. After this prepare, there's a second foreach loop. But the function does NOT reach after the prepare statement.
I would like to know the reason. MYSQL says the following:
Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INSERT INTO gosurveys_surveys_questions_answers
SET survey_id = ?, ques'
The name of the parameter is not the important thing.
If you want to run a query more than once then preparing it is a great idea but the parameter names are almost irrelevant. the important thing is the binding of new values to the paramters.
So lets assume this is your query
$sql = "INSERT INTO gosurveys_surveys_questions_answers
SET survey_id = :a,
question_id = :b,
user_email = :c,
answer_type = :d,
question_answer = :e,
question_answer_creation_date = UTC_TIMESTAMP()";
Notice the parameter names are irrelevant as long as they are something unique in the query string.
So now you prepare that query. This passes the basic query to the database where it is compiled and optimized, but is not actually run.
$stmt = $this->conn->prepare($sql);
Now within a loop that gets you the parameter you can run that prepared query 1000, 1,000,000 times if you like, all you have to do is bind new values to the parameters and execute the query, which passes the parameter values to the already prepared (compiled and optimized query) and runs it with the data you pass on the execute()
foreach($inArray as $array) {
// build the array of parameters and values
$params = [ ':a' => $array['field1']
':b' => $array['field2']
':c' => $array['field3']
':d' => $array['field4']
':e' => $array['field5']
];
// execute the prepared query using the new parameters
$stmt->execute($params);
}
here is the way to insert multiple rows of data, you prepare once and insert execute multiple times one prepared statement
$dbh = new PDO($dsn, $user, $pass, $options);
$arrayOfData = [...];
$stmt = $dbh->prepare('INSERT INTO table SET col = :val');
$dbh->beginTransaction();
foreach($arrayOfData as $data) {
$stmt->bindValue(':val', $data);
$stmt->execute();
}
$dbh->commit();
The idea of a prepared statement is that the statement is prepared once and then executed multiple times. Something like this:
$sql = 'INSERT INTO gosurveys_surveys_questions_answers
SET survey_id = :survey_id,
question_id = :question_id,
user_email = :user_email,
answer_type = :answer_type,
question_answer = :question_answer,
question_answer_creation_date = UTC_TIMESTAMP()';
$query = $this->conn->prepare($sql);
foreach($questionsArray as $questionAnswerRow) {
$query->execute([
":survey_id" => $questionAnswerRow["survey_id"],
// etc.
]);
}

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

Select table from Sql server and insert data to Mysql table

I have a running ms sql server and want some data copied to a mysql database.
i already can connect to them both so i made something like:
$pdo = new PDO('SQLSERVER', $user, $password);
$sql = "SELECT id, name FROM users";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$json_users = array();
while ($row = $stmt->fetchObject()){
$json_users[] = $row;
}
$pdo = new PDO('MYSQLDB', $user, $password);
foreach ($json_users as $key => $value){
$sql = "INSERT INTO users (id, name) VALUES (:id, :name)"
$stmt = $pdo->prepare($sql);
$stmt->bindParam('id', $value->id, PDO::PARAM_INT);
$stmt->bindParam('name', $value->name, PDO::PARAM_STR);
$stmt->execute();
}
this does work but takes a lot of time cause its a big table.
so my question is can i insert the complete results from sqlserver to mysql at once with only one insert query? without the foreach?
Update: the table contains 173398 rows and 10 columns
With prepared statements (especially for multi-insert) you want to have your prepared statement outside your loop. You only need to set the query up once, then supply your data in each subsequent call
$sql = "INSERT INTO users (id, name) VALUES (:id, :name)";
$stmt = $pdo->prepare($sql);
foreach($json_users as $key => $value){
$stmt->bindParam('id', $value->id, PDO::PARAM_INT);
$stmt->bindParam('name', $value->name, PDO::PARAM_STR);
$stmt->execute();
}
You can export this into CSV file first from MSSQL then import that file into MySQL.
$pdo = new PDO('SQLSERVER', $user, $password);
$sql = "SELECT id, name FROM users";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$fp = fopen('/tmp/mssql.export.csv', 'w');
while ($row = $stmt->fetch(PDO::FETCH_NUM)){
fputcsv($fp, array_values($row));
}
fclose($fp);
$pdo = new PDO('MYSQLDB', $user, $password, array(PDO::MYSQL_ATTR_LOCAL_INFILE => 1));
$sql = <<<eof
LOAD DATA LOCAL INFILE '/tmp/mssql.export.csv'
INTO TABLE user_copy
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
(id,name)
eof;
$pdo->exec($sql);
one drawback with the above, you need to have this configuration enabled in my.cnf ( MySQL configuration )
[server]
local-infile=1
Since MySQL cannot read files that are owned by others unless it it opened with --local-infile=1
I would suggest not bind values, but generate the query string:
$sql = "";
foreach ($json_users as $key => $value){
if ($sql=="") {
$sql = "INSERT INTO users (id, name) VALUES ";
$sql =." (".$value->id.',"'.$value->name.'")';
} else {
$sql .= ", (".$value->id.',"'.$value->name.'")';
}
}
$stmt = $pdo->prepare($sql);
$stmt->execute();
It is not best practice, but since you trust data source it could help.
Consider doing bulk inserts instead of one row at a time.
$sourcedb = new \PDO('SQLSERVER', $sourceUser, $sourcePassword);
$targetdb = new \PDO('MYSQLDB', $targetUser, $targetPassword);
$sourceCountSql = "SELECT count(*) count FROM users;";
/**
* for mssql server 2005+
*/
$sourceSelectSql = "
SELECT
id,
name
FROM
(
SELECT
ROW_NUMBER() OVER (ORDER BY id) RowNum,
id,
name
FROM
users
) users
WHERE
RowNum >= %d
AND RowNum < %d
ORDER BY
RowNum
";
/**
* for mssql server 2012+
*/
$sourceSelectSql = "
SELECT
FROM TableName ORDER BY id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;
SELECT
id,
name
FROM
users
ORDER BY
id
OFFSET %d ROWS
FETCH NEXT %d ROWS ONLY
";
$sourceCount = $sourcedb->query($sourceCountSql, \PDO::FETCH_COLUMN, 0);
$rowCount = 1000;
$count = 0;
$count2 = 0;
for($x = 0; $x < $sourceCount; $x += $rowCount) {
$sourceRecords = $sourcedb->query(sprintf($sourceSelectSql, $x, $rowCount), \PDO::FETCH_ASSOC);
$inserts = [];
foreach($sourceRecords as $row) {
$inserts[] = sprintf("(:id_%1$d, :name_%1$d)", $count++);
}
$stmt = $targetdb->prepare(sprintf("INSERT INTO users (id, name) VALUES %s;", implode(',', $inserts));
foreach($sourceRecords as $row) {
$stmt->bindParam(sprintf('id_%d', $count2), $row['id'], \PDO::PARAM_INT);
$stmt->bindParam(sprintf('name_%d', $count2), $row['name'], \PDO::PARAM_STR);
$count2++;
}
$targetdb->execute();
unset($inserts);
}

Ignore particular WHERE criteria

I want to execute a parameterized query to perform a search by user-supplied parameters. There are quite a few parameters and not all of them are going to be supplied all the time. How can I make a standard query that specifies all possible parameters, but ignore some of these parameters if the user didn't choose a meaningful parameter value?
Here's an imaginary example to illustrate what I'm going for
$sql = 'SELECT * FROM people WHERE first_name = :first_name AND last_name = :last_name AND age = :age AND sex = :sex';
$query = $db->prepare($sql);
$query->execute(array(':first_name' => 'John', ':age' => '27');
Obviously, this will not work because the number of provided parameters does not match the number of expected parameters. Do I have to craft the query every time with only the specified parameters being included in the WHERE clause, or is there a way to get some of these parameters to be ignored or always return true when checked?
SELECT * FROM people
WHERE (first_name = :first_name or :first_name is null)
AND (last_name = :last_name or :last_name is null)
AND (age = :age or :age is null)
AND (sex = :sex or :sex is null)
When passing parameters, supply null for the ones you don't need.
Note that to be able to run a query this way, emulation mode for PDO have to be turned ON
First, start by changing your $sql string to simply:
$sql = 'SELECT * FROM people WHERE 1 = 1';
The WHERE 1 = 1 will allow you to not include any additional parameters...
Next, selectively concatenate to your $sql string any additional parameter that has a meaningful value:
$sql .= ' AND first_name = :first_name'
$sql .= ' AND age = :age'
Your $sql string now only contains the parameters that you plan on providing, so you can proceed as before:
$query = $db->prepare($sql);
$query->execute(array(':first_name' => 'John', ':age' => '27');
If you can't solve your problem by changing your query... There are several libraries that help with assembling queries. I've used Zend_Db_Select in the past but every framework likely has something similar:
$select = new Zend_Db_Select;
$select->from('people');
if (!empty($lastName)) {
$select->where('lastname = ?', $lastname);
}
$select->order('lastname desc')->limit(10);
echo $select; // SELECT * FROM people WHERE lastname = '...' ORDER BY lastname desc LIMIT 10
I've tested the solution given by #juergen but it gives a PDOException since number of bound variables does not match. The following (not so elegant) code works regardless of no of parameters:
function searchPeople( $inputArr )
{
$allowed = array(':first_name'=>'first_name', ':last_name'=>'last_name', ':age'=>'age', ':sex'=>'sex');
$sql = 'SELECT * FROM sf_guard_user WHERE 1 = 1';
foreach($allowed AS $key => $val)
{
if( array_key_exists( $key, $inputArr ) ){
$sql .= ' AND '. $val .' = '. $key;
}
}
$query = $db->prepare( $sql );
$query->execute( $inputArr );
return $query->fetchAll();
}
Usage:
$result = searchPeople(array(':first_name' => 'John', ':age' => '27'));

Categories