db_merge を使用したクエリのマージ

~ Merge queries using db_merge 和訳 ~

Merge query は少し特殊なクエリです。SQL 2003 の仕様では定義されているものですが、ほとんどのデータベースは、標準の構文をサポートしていません。ほとんどがそれぞれデータベース固有の構文の実装で代替されています。Drupal のマージクエリは抽象化されたオブジェクトであり、それぞれのデータベースにおける適切な構文にコンパイルすることが出来ます。これは「UPDATE」と「INSERT」を足し合わせたようなもので、しばしば「UPSERT」と呼ばれます。

一般的に、マージクエリは INSERT クエリと UPDATE クエリを合わせたものです。指定したプライマリキーを持つレコードが既に存在する場合は、それを更新し、存在しない場合は、新たなレコードとしてインサートします。つまり次のような構文と同等です。

<?php
if (db_query("SELECT COUNT(*) FROM {example} WHERE id = :id", array(':id' => $id))->fetchField()) {
  // Run an update using WHERE id = $id
}
else {
  // Run an insert, inserting $id for id 
}
?>

実際にはデータベースによって大きく異なります。Merge query は不可分操作の概念ですが、データベースによっては厳密には不可分操作でない場合があります。上の例は不可分操作ではありませんが、MySQL では不可分操作として実装されます。

Merge query の記述例は以下のようになります。

単純に値をセットする場合

<?php
db_merge('example')
  ->key(array('name' => $name))
  ->fields(array(
      'field1' => $value1,
      'field2' => $value2,
  ))
  ->execute();
?>

この例では、「example」テーブルを操作します。キー「name」の値に $name を指定し、そしてそこに挿入したい値の配列を渡します。

「name」フィールドの値に $name を持つレコードが既に存在する場合、field1、field2 のフィールドはそれぞれ対応する値がセットされアップデートされます。そのようなレコードがまだ存在しない場合、「name」フィールドの値に $name、「field1」フィールドの値に $value1、「field2」フィールドの値に $value2 を持つレコードが新たにインサートされます。このように、既存のレコードがあるか否かに関わらず、結果的には同一のレコードが作られます。

条件に応じて値をセットする場合

ときに、キーの値を持つレコードが存在するかどうかによって、どの値を挿入するかが異なる場合があります。その場合は 2 つの方法があります。

<?php
db_merge('example')
  ->insertFields(array(
      'field1' => $value1,
      'field2' => $value2,
  ))
  ->updateFields(array(
    'field1' => $alternate1,
  ))
  ->key(array('name' => $name))
  ->execute();
?>

上の例では、該当するレコードが存在しない場合においては最初の例と同じですが、既に存在している場合は「field1」には $value1 ではなく $alternate1 という値をセットし、「field2」の値はそのままでアップデートします。updateField() メソッドはフィールドとその値を示す連想配列か、もしくはフィールドの配列と、その値の配列からなる添字配列を受け付けます。

<?php
db_merge('example')
  ->key(array('name' => $name))
  ->fields(array(
      'field1' => $value1,
      'field2' => $value2,
  ))
  ->expression('field1', 'field1 + :inc', array(':inc' => 1))
  ->execute();
?>

この例では、レコードが既に存在する場合、field1 の値には既存の値に 1 を足した値がセットされます。これは、たとえば何らかのイベントに応じて値が増えていくような「カウンター」の動作を実装する際に便利なものです。field2 はこの場合既存のレコードがあるか否かに関わらず、同じ値がセットされます。

expression() メソッドは、それぞれのフィールドに対して、レコードが存在する場合にセットする expression をひとつずつ、複数設置することが出来ます。ひとつめのパラメータは対象のフィールド、ふたつめはそのフィールドにセットする expression を示す SQL フラグメント、三つ目のパラメータはオプションで、expression にのプレイスホルダーに代入する値を示す配列です。

expression() で指定するフィールドは必ずしも fields() にセットされている必要はありません。

優先順位

この API では、論理的にまったくおかしなクエリも発行できてしまいます。レコードが存在する場合に、あるフィールドに対して値をそのままにしておくのか、expression をセットするのか。潜在的なエラーを回避するため、次のルールが適用されます。

  • フィールドに expression() がセットされている場合は、update() よりも優先する。
  • update() に値が指定されている場合は、レコードが存在する場合のみ変更される。update() に指定が無い場合は、そのまま変更されない。

それでもなお、おかしなクエリが発行できてしまいます。そのあたりの確認をしっかりするかどうかは開発者次第です。

コア: 
Drupal7