如何使用 PHP SQL 准备语句避免代码重复?
How to avoid code repetition with PHP SQL prepared statements?
在我看到的大多数 SQL PHP 准备语句的示例中,例如:
$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,
]);
字段名称几乎重复... 4 次!
- 在
INSERT INTO(...)
之后一次:task_name
(SQL 中的列名)
- 在
VALUES(...)
之后::task_name
- 一次在字典键中:
:task_name
- 曾经在字典中的值:
$taskName
(局部变量)
我知道每个都有不同的含义,但是,这种冗余确实很烦人:如果我们想在查询中更改某些内容,我们必须更改它 4 次!
如何更好地准备语句避免 PHP 中的过多冗余?
这不是多余的,它是最不复杂的例子。
当您必须使用不同的参数多次执行查询时,准备好的语句的优势会发挥更大的作用。
以下示例将按 value
从 1 到 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();
}
您也可以自由使用其他 php 逻辑来绑定参数:
$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();
这是一个很好的问题,我有几个答案。
原始 PHP
首先,您可以使用一些技巧来减少冗长,例如在查询中省略字段子句(并在缺少字段的值子句中添加默认值)和使用位置占位符:
$sql = 'INSERT INTO tasks VALUES(null, ?, ?, ?)';
$this->pdo->prepare($sql)->execute([$taskName, $startDate, $completedDate]);
我称它们为技巧,因为它们并不总是适用。
请注意,您必须为 table 中的所有列提供一个值。它可以只是一个 null
值,或者,要使其与省略的字段 100% 等效,您可以将其设置为 DEFAULT(field_name)
这样它将插入一个在 table 定义中定义的默认值.
辅助函数
下一个级别是为插入创建辅助函数。这样做时,必须敏锐地意识到SQL Injection through field names。
因此,这样的辅助函数必须有自己的辅助函数:
function escape_mysql_identifier($field){
return "`".str_replace("`", "``", $field)."`";
}
有了这样的函数,我们可以创建一个辅助函数,它接受一个 table 名称和一个包含 field name => value
对的数据数组:
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));
}
我故意不在这里使用命名占位符,因为它使代码更短,占位符名称中可能不允许使用字符,但对列名完全有效,例如 space 或破折号;也因为我们通常不关心它在内部是如何工作的。
现在您的插入代码将变为
prepared_insert($this->pdo, 'tasks',[
'task_name' => $taskName,
'start_date' => $startDate,
'completed_date' => $completedDate,
]);
删除了太多重复项
一个婴儿 ORM
不过我也不喜欢上面的解决方案,里面有一些怪癖。
为了满足自动化的需要,我宁愿创建一个简单的 ORM。不要被这个词吓到,它并不像某些人想象的那样可怕。我最近发布了一篇 ,因此您也可以将其用于您的案例,特别是考虑到您已经在使用 OOP。
只需输入一个insert()
方法
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);
}
之后你将不得不准备你的 class,
class Task extends BaseAcctiveRecord
{
protected $_table = "tasks";
protected $_fields = ['task_name', 'start_date', 'completed_date'];
}
然后 - 所有的魔法都在这里发生! - 您根本不需要编写插入代码! 只需创建 class 的新实例,为其属性赋值,然后调用 insert()
方法:
include 'pdo.php';
$task = new Task($pdo);
$task->task_name = $taskName;
$task->start_date = $startDate;
$task->completed_date = $completedDate;
$user->insert();
在我看到的大多数 SQL PHP 准备语句的示例中,例如:
$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,
]);
字段名称几乎重复... 4 次!
- 在
INSERT INTO(...)
之后一次:task_name
(SQL 中的列名) - 在
VALUES(...)
之后::task_name
- 一次在字典键中:
:task_name
- 曾经在字典中的值:
$taskName
(局部变量)
我知道每个都有不同的含义,但是,这种冗余确实很烦人:如果我们想在查询中更改某些内容,我们必须更改它 4 次!
如何更好地准备语句避免 PHP 中的过多冗余?
这不是多余的,它是最不复杂的例子。
当您必须使用不同的参数多次执行查询时,准备好的语句的优势会发挥更大的作用。
以下示例将按 value
从 1 到 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();
}
您也可以自由使用其他 php 逻辑来绑定参数:
$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();
这是一个很好的问题,我有几个答案。
原始 PHP
首先,您可以使用一些技巧来减少冗长,例如在查询中省略字段子句(并在缺少字段的值子句中添加默认值)和使用位置占位符:
$sql = 'INSERT INTO tasks VALUES(null, ?, ?, ?)';
$this->pdo->prepare($sql)->execute([$taskName, $startDate, $completedDate]);
我称它们为技巧,因为它们并不总是适用。
请注意,您必须为 table 中的所有列提供一个值。它可以只是一个 null
值,或者,要使其与省略的字段 100% 等效,您可以将其设置为 DEFAULT(field_name)
这样它将插入一个在 table 定义中定义的默认值.
辅助函数
下一个级别是为插入创建辅助函数。这样做时,必须敏锐地意识到SQL Injection through field names。
因此,这样的辅助函数必须有自己的辅助函数:
function escape_mysql_identifier($field){
return "`".str_replace("`", "``", $field)."`";
}
有了这样的函数,我们可以创建一个辅助函数,它接受一个 table 名称和一个包含 field name => value
对的数据数组:
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));
}
我故意不在这里使用命名占位符,因为它使代码更短,占位符名称中可能不允许使用字符,但对列名完全有效,例如 space 或破折号;也因为我们通常不关心它在内部是如何工作的。
现在您的插入代码将变为
prepared_insert($this->pdo, 'tasks',[
'task_name' => $taskName,
'start_date' => $startDate,
'completed_date' => $completedDate,
]);
删除了太多重复项
一个婴儿 ORM
不过我也不喜欢上面的解决方案,里面有一些怪癖。
为了满足自动化的需要,我宁愿创建一个简单的 ORM。不要被这个词吓到,它并不像某些人想象的那样可怕。我最近发布了一篇
只需输入一个insert()
方法
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);
}
之后你将不得不准备你的 class,
class Task extends BaseAcctiveRecord
{
protected $_table = "tasks";
protected $_fields = ['task_name', 'start_date', 'completed_date'];
}
然后 - 所有的魔法都在这里发生! - 您根本不需要编写插入代码! 只需创建 class 的新实例,为其属性赋值,然后调用 insert()
方法:
include 'pdo.php';
$task = new Task($pdo);
$task->task_name = $taskName;
$task->start_date = $startDate;
$task->completed_date = $completedDate;
$user->insert();