问题
In most examples of SQL PHP prepared statements that I see, such as:
$sql = 'INSERT INTO tasks(task_name, start_date, completed_date) VALUES(:task_name, :start_date, :completed_date)';
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':task_name' => $taskName,
':start_date' => $startDate,
':completed_date' => $completedDate,
]);
the field names are nearly repeated ... 4 times!
- once after the
INSERT INTO(...)
:task_name
(column name in SQL) - once after the
VALUES(...)
::task_name
- once in the dictionary key:
:task_name
- once in the dictionary value:
$taskName
(local variable)
I understand that each of these have a different meaning, but still, this redundancy is really annoying: if we want to change something in the query, we have to change it 4 times!
How to have a better prepared statement avoiding so much redundancy in PHP?
回答1:
That's a very good question and I have several answers for it.
Raw PHP
First of all, you can use several tricks to reduce the verbosity, like omitting the fields clause in the query (and adding default values in the values clause for the missing fields) and using positional placeholders:
$sql = 'INSERT INTO tasks VALUES(null, ?, ?, ?)';
$this->pdo->prepare($sql)->execute([$taskName, $startDate, $completedDate]);
I call them tricks because they aren't always applicable.
Note that you must provide a value for all the columns in the table. It could be simply a null
value or, to make it 100% equivalent for the omitted field, you could have it as DEFAULT(field_name)
so it will insert a default value defined in the table definition.
A helper function
The next level would be creation of a helper function for inserts. When doing this, one must be acutely aware of the SQL Injection through field names.
Hence, such a helper function must have a helper function of its own:
function escape_mysql_identifier($field){
return "`".str_replace("`", "``", $field)."`";
}
Having such a function we can create a helper function that accepts a table name and a data array contains field name => value
pairs:
function prepared_insert($conn, $table, $data) {
$keys = array_keys($data);
$keys = array_map('escape_mysql_identifier', $keys);
$fields = implode(",", $keys);
$table = escape_mysql_identifier($table);
$placeholders = str_repeat('?,', count($keys) - 1) . '?';
$sql = "INSERT INTO $table ($fields) VALUES ($placeholders)";
$conn->prepare($sql)->execute(array_values($data));
}
I am intentionally not using named placeholders here because it makes the code shorter, there could be characters disallowed in placeholder names while being perfectly valid for the column names, a space or a dash for example; and also because we generally don't care how it works inside.
Now your insert code will become
prepared_insert($this->pdo, 'tasks',[
'task_name' => $taskName,
'start_date' => $startDate,
'completed_date' => $completedDate,
]);
with so much repetitions removed
A baby ORM
However I don't like the above solution either, there are some quirks in it.
To fulfill the need for the automation, I would rather create a simple ORM. Don't be scared by the term it is not as monstrous as some picture it. I have a complete working example posted recently so you can use it for your case as well, especially given you are already using OOP.
Just throw in an insert()
method
public function insert()
{
$fields = '`'.implode("`,`", $this->_fields).'`';
$placeholders = str_repeat('?,', count($this->_fields) - 1) . '?';
$data = [];
foreach($this->_fields as $key)
{
$data[] = $this->{$key};
}
$sql = "INSERT INTO `{$this->_table}` ($fields) VALUES ($placeholders)";
$this->_db->prepare($sql)->execute($data);
}
After that you will have to prepare your class,
class Task extends BaseDataMapper
{
protected $_table = "tasks";
protected $_fields = ['task_name', 'start_date', 'completed_date'];
}
and then - all the magic happens here! - you won't have to write the insert code at all! Instead just create a new instance of your class, assign values to its properties and then just call the insert()
method:
include 'pdo.php';
$task = new Task($pdo);
$task->task_name = $taskName;
$task->start_date = $startDate;
$task->completed_date = $completedDate;
$user->insert();
回答2:
This is not redundant its the least complex example.
The advanages of prepared statements come more into play when you have to execute a query multiple times with diffrent params.
The following example will fetch all rows by value
from 1 to 10:
$value = 1;
$stmt = $pdo->prepare("INSER INTO (first, second, third) VALUES (?, ?, ?);");
$rowsToInsert = [["first", "second", "third"]];
foreach ($rowsToInsert as $row) {
array_map($row, function($v, $i) use ($stmt) {
$stmt->bindValue($i + 1, $v);
});
$stmt->execute();
}
Also you are free to use other php logic to bind params:
$params = [
":first" => $first,
":second" => $second,
":third" => $third
];
$sql = sprintf(
"INSERT INTO (first, second, third) VALUES (%s);",
implode(" ", array_keys($params))
);
$stmt = $pdo->prepare($sql);
foreach ($params as $name => $value) {
$stmt->bindValue($name, $value);
}
$stmt->execute();
来源:https://stackoverflow.com/questions/58439799/how-to-avoid-code-repetition-with-php-sql-prepared-statements