【问题标题】:How to avoid code repetition with PHP SQL prepared statements?如何使用 PHP SQL 准备语句避免代码重复?
【发布时间】:2020-02-14 19:09:43
【问题描述】:

在我看到的大多数 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 中出现如此多的冗余?

【问题讨论】:

  • 不要使用名称,而是使用占位符 (?)。就我个人而言,我喜欢使用这些名称,因为它非常清楚事物之间的关系。
  • @Dave 你能举个例子吗?当变量很多时,冗余变得可见。
  • 这就是您使用ORM (Object-Relational Mapper) 的原因。您无需手动构建讨厌的、容易注入的 SQL 字符串,而是使用适当的对象并设置它们的属性和状态,然后告诉 ORM 持久化更改,它会为您生成并执行所有 SQL。
  • 构建您自己的 CRUD 对象,将您的数据库对象注入其中。然后您的代码可以调用您自己的自定义update() 方法。然后,您可以看到 $tableName->update($id, [‘field’=>’value’] 之类的东西,而不是所有让您烦恼的东西
  • 我会努力解决的;不知道我有多少时间。哦,我的意思是插入,可以像$tableName->insert(['field1'=>'value1','field2'=>'value2']);

标签: php sql sqlite prepared-statement sql-insert


【解决方案1】:

这是一个很好的问题,我有几个答案。

原始 PHP

首先,您可以使用一些技巧来减少冗长,例如在查询中省略 fields 子句(并在 values 子句中为缺少的字段添加默认值)和使用位置占位符:

$sql = 'INSERT INTO tasks VALUES(null, ?, ?, ?)';
$this->pdo->prepare($sql)->execute([$taskName, $startDate, $completedDate]);

我称它们为技巧是因为它们并不总是适用。

请注意,您必须为表中的所有列提供值。它可能只是一个 null 值,或者为了使其与省略的字段 100% 等效,您可以将其设置为 DEFAULT(field_name),这样它就会插入一个在表定义中定义的默认值。

辅助函数

下一个级别是为插入创建一个辅助函数。执行此操作时,必须敏锐地意识到SQL Injection through field names

因此,这样的辅助函数必须有自己的辅助函数:

function escape_mysql_identifier($field){
    return "`".str_replace("`", "``", $field)."`";
}

有了这样一个函数,我们可以创建一个辅助函数,它接受一个表名和一个包含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));
} 

我故意不在这里使用命名占位符,因为它使代码更短,占位符名称中可能存在不允许的字符,而对于列名、空格或破折号完全有效;也因为我们通常不在乎它在内部是如何工作的。

现在您的插入代码将变为

prepared_insert($this->pdo, 'tasks',[
    'task_name' => $taskName,
    'start_date' => $startDate,
    'completed_date' => $completedDate,
]);

删除了很多重复

一个婴儿 ORM

但是我也不喜欢上面的解决方案,它有一些怪癖。

为了满足自动化的需求,我宁愿创建一个简单的 ORM。不要被这个词吓到它并不像一些想象的那么可怕。我最近发布了complete working example,因此您也可以将其用于您的案例,尤其是考虑到您已经在使用 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 Task extends BaseAcctiveRecord
{
    protected $_table = "tasks";
    protected $_fields = ['task_name', 'start_date', 'completed_date'];
}

然后——所有的魔法都发生在这里! - 您根本不需要编写插入代码! 只需创建类的新实例,为其属性赋值,然后调用insert() 方法:

include 'pdo.php';
$task = new Task($pdo);
$task->task_name = $taskName;
$task->start_date = $startDate;
$task->completed_date = $completedDate;
$user->insert();

【讨论】:

  • 非常感谢,答案也很有趣!我会选择$pdo->prepare('INSERT INTO tasks VALUES(null, ?, ?, ?)')->execute([$taskName, $startDate, $completedDate]);,它很干净。
  • PS:这里的一个技巧是避免写所有列名INSERT INTO tasks(col1, col2, col3, ...) VALUES (?, ?, ?, ...),而只写INSERT INTO tasks VALUES(?, ?, ?, ...)。这仅在您插入 all 列时有效,但是当您插入 all 列时是否有类似的技巧,除了 id integer primary key autoincrement 列(对于这一列,我们不希望? 但我们希望它自动填充)
  • @Basj 这正是我在回答中所说的,所以 id 有一个 null 值。对于此语法,您不能省略 values 子句中的字段,无论它是否为主键。
  • 太棒了!你说它的字符比我解释它的尝试少了大约 12,000 个字符... 有趣的是,在 insert() 方法中迭代对象的需要刚刚出现在我正在处理的项目中。谈及时!
【解决方案2】:

这不是多余的,它是最简单的例子。
当您必须使用不同的参数多次执行查询时,预准备语句的优势会发挥更大的作用。

以下示例将通过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();

【讨论】:

  • 感谢您的回答。为了与原始问题有相似之处,使用 INSERT 而不是 SELECT 会很有趣,因为使用 INSERT 需要将语句与局部变量绑定(我添加了标签 sql-insert)。
  • 另外,您提供的示例只有一个变量value,当您有多个变量时,冗余变得更加明显。
  • PS:在您的更新版本中,您仍然有:first$firstINSERT INTO (first...):这就是我所说的冗余。
  • @Basj 你也可以说在每个变量声明之前输入$ 是多余的。它就是这样。在我的新编辑中,我添加了一个插入示例,即使您尝试保持简短而不是“冗余”,您也必须输入一些代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-03-02
  • 1970-01-01
  • 2022-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多