合并查询 merge
合并查询是一种特殊类型的混合查询。虽然它们的语法在 SQL 2003 规范中被定义,但几乎没有数据库支持该标准语法。然而,大多数数据库提供了使用数据库特定语法的替代实现。Drupal 中的合并查询构建器将合并查询的概念抽象为一个结构化对象,可以根据每个数据库生成相应的语法。有时它们也被称为 “UPSERT” 查询,即 UPDATE 和 INSERT 的组合。
从总体上看,合并查询就是插入和更新查询的结合。如果满足某个条件,例如具有指定主键的行已经存在,则会执行更新查询。如果不存在,则执行插入查询。在最常见的情况下,这等同于:
if ($connection->query("SELECT COUNT(*) FROM {example} WHERE id = :id", [':id' => $id])->fetchField()) {
// 使用 WHERE id = $id 执行更新
}
else {
// 执行插入,为 id 插入 $id
}
实际实现因数据库而异。请注意,虽然合并查询在概念上是一个原子操作,但它是否真正是原子的取决于具体数据库的实现。例如,MySQL 的实现是单独的原子查询,但上面的简化情况则不是。
合并查询最常见的用法如下所示。
直接设置
$connection->merge('example')
->key('name', $name)
->fields([
'field1' => $value1,
'field2' => $value2,
])
->execute();
在上面的示例中,我们指定了查询操作的表为 “example”。接着我们指定了一个键字段 'name',其值为 $name。然后我们提供了一个要设置的值数组。
如果已经存在一条 name 字段等于 $name 的记录,那么该记录的 field1 和 field2 会被更新为相应的值。如果不存在这样的记录,就会新建一条记录,其中 name = $name,field1 = $value1,field2 = $value2。因此,无论记录是否存在,最终结果保持一致。
条件设置
在某些情况下,可能需要根据记录是否存在(由 key() 字段决定)来以不同方式设置值。可以通过两种方式来实现。
$connection->merge('example')
->insertFields([
'field1' => $value1,
'field2' => $value2,
])
->updateFields([
'field1' => $alternate1,
])
->key('name', $name)
->execute();
上面的示例与第一个类似,但如果记录已存在并且我们进行更新,则 field1 的值会设置为 $alternate1 而不是 $value1,field2 则不受影响。updateFields() 方法可以接受一个关联数组,也可以接受两个平行的数字索引数组,一个表示字段,一个表示值,且顺序必须对应。
$connection->merge('example')
->key('name', $name)
->fields([
'field1' => $value1,
'field2' => $value2,
])
->expression('field1', 'field1 + :inc', [':inc' => 1])
->execute();
在这个示例中,如果记录已经存在,field1 的值会在原有基础上加 1。这对于“计数类查询”非常有用,例如当某个事件发生时你希望数据库中的计数器递增一次。field2 无论记录是否存在都会被设置为相同的值。
请注意,expression() 可以被多次调用,每个字段一次,用于定义当记录已存在时该字段的表达式赋值。第一个参数是字段名,第二个参数是 SQL 表达式片段,指定字段应赋予的表达式,第三个可选参数是一个占位符数组,用于替换表达式中的值。
并不要求 expression() 使用的字段必须已经出现在 fields() 中。
由于以上 API 的灵活性,完全可能定义一些没有逻辑意义的查询,例如如果为某个字段同时指定了忽略和使用表达式赋值。为了最小化可能的错误,应用了以下规则:
- 如果某个字段设置了 expression(),它的优先级高于 updateFields()。
- 如果在 updateFields() 中指定了字段,则仅这些字段会在记录已存在时被更新。未在 updateFields() 中指定的字段不会受到影响。
请注意,仍然有可能定义出没有意义的查询。开发者必须确保不要编写出这种无意义的查询,否则其行为是未定义的。