WP_Query の meta_query が 'relation' => 'OR' のとき、その配列の末尾に条件を1つ足すと、足した条件も OR の選択肢の1つになります。絞り込むつもりで足しても、結果は減らずに増えます。WordPress 7.1.2 で、投稿6件に対して測りました。
| meta_query | 件数 | 返ってきた投稿 |
|---|---|---|
| A. 元の条件(red OR blue) | 4件 | red, blue, red(生成画像), blue(生成画像) |
| B. A の末尾に「生成画像を除く」を足す | 5件 | red, blue, green, red(生成画像), blue(生成画像) |
| C. A と「生成画像を除く」を AND で包む | 2件 | red, blue |
(投稿は red・blue・green が各2件で、うち各1件に生成画像の印を付けたもの)
B は、除くつもりの2件が残ったまま、元の条件に合わない green まで返ってきます。
| 元の条件 | 末尾に足す | AND で包む |
|---|---|---|
relation が OR |
広がる(4件→5件) | 絞られる(4件→2件) |
relation を省略(AND 扱い) |
絞られる(2件→1件) | 絞られる(2件→1件) |
| meta_query が無い | 同じ(3件) | 同じ(3件) |
先に断っておくと、これは WP_Query のドキュメントに書いてあるとおりの動きです。relation は「The logical relationship between each inner meta_query array when there is more than one.」で、同じ階層にある配列すべてにかかります。meta_query は入れ子にできる(4.1 から)とも書かれています。ここで書くのは、それを気にせず他のプラグインのクエリに条件を足していた自分のプラグインで何が起きたかと、実際に測った結果です。
何が起きたか
自作の Rapls PDF Image Creator には、自動で作ったサムネイル画像をメディアライブラリの一覧から隠す設定があります。隠すために、メディアのクエリの meta_query に条件を足していました。1.4.20 までは、こう書いていました。
// Exclude thumbnails created by this plugin
$metaQuery = $query->get('meta_query') ?: [];
$metaQuery[] = [
'relation' => 'OR',
[
'key' => '_rapls_pic_is_thumbnail',
'compare' => 'NOT EXISTS',
],
[
'key' => '_rapls_pic_is_thumbnail',
'value' => '1',
'compare' => '!=',
],
];
$query->set('meta_query', $metaQuery);
足している条件そのものは、内側で OR を使っていても、1つの配列にまとまっているので問題ありません。問題は、元の $metaQuery の relation が何かを見ずに、その末尾に足していたことです。
自分のプラグインだけが使うクエリなら、元の meta_query は空か、AND です。そのときは、表の2行目と3行目のとおり、末尾に足しても正しく絞られます。ところが、このフックは他のプラグインのクエリにもかかっていました。ほかのプラグインが OR で条件を組んでいると、その中に「生成画像ではない」という選択肢が1つ増えるだけです。
SQL で見ると
B と C の WHERE の meta 部分です(WP_Query::$request から取り出して整形しました)。
-- B: 末尾に足した場合
( ( wp_postmeta.meta_key = 'color' AND wp_postmeta.meta_value = 'red' )
OR ( wp_postmeta.meta_key = 'color' AND wp_postmeta.meta_value = 'blue' )
OR ( mt1.post_id IS NULL
OR ( mt2.meta_key = '_rapls_pic_is_thumbnail' AND mt2.meta_value != '1' ) ) )
-- C: AND で包んだ場合
( ( ( wp_postmeta.meta_key = 'color' AND wp_postmeta.meta_value = 'red' )
OR ( wp_postmeta.meta_key = 'color' AND wp_postmeta.meta_value = 'blue' ) )
AND ( mt1.post_id IS NULL
OR ( mt2.meta_key = '_rapls_pic_is_thumbnail' AND mt2.meta_value != '1' ) ) )
B では、足した条件が3つ目の OR になっています。生成画像の印が無い投稿は、色が何であっても3つ目で当たります。だから green が入ってきます。生成画像の2件は、1つ目か2つ目の色の条件で当たるので、除かれません。
C では、元の2つの OR がかっこでまとまり、そこに AND で足した条件がかかっています。
直し方:元の meta_query を1つの要素にして、AND で包む
1.4.21 で、こう直しました。
public static function withoutGeneratedImages($metaQuery): array
{
$hide = [
'relation' => 'OR',
[
'key' => '_rapls_pic_is_thumbnail',
'compare' => 'NOT EXISTS',
],
[
'key' => '_rapls_pic_is_thumbnail',
'value' => '1',
'compare' => '!=',
],
];
if (!is_array($metaQuery) || [] === $metaQuery) {
return [$hide];
}
return ['relation' => 'AND', $metaQuery, $hide];
}
元の meta_query を、relation ごとそのまま1つの要素にします。中が OR でも AND でも、元の条件の意味は変わりません。そのうえで、外側を AND にして自分の条件を並べます。表のとおり、元が OR でも AND でも、正しく絞られます。
元の relation を調べて分岐する方法もありますが、包むほうが単純です。入れ子は 4.1 から使えるので、今の WordPress で困ることはありません。
そもそも、他のプラグインのクエリにかけない
この件を直したあと、1.4.22 でもう1つ直しています。「生成画像を隠す」が、メディアライブラリの一覧に限らず、管理画面のすべての添付ファイルのクエリにかかっていました。他のプラグインが自分の画面で添付ファイルを探すクエリにも、条件を足していたことになります。
包み方を直せば結果は正しくなります。それでも、他人のクエリに条件を足すこと自体が、今回の件の入り口でした。1.4.22 からは、メディアライブラリの一覧のクエリにだけかけています。
pre_get_posts で条件を足すとき
- 元の
meta_queryの末尾に足さない。['relation' => 'AND', $元の条件, $自分の条件]で包む - 元が空なら
[$自分の条件]で足りる -
tax_queryも同じ形でrelationを持つ。足すときは同じように包む - そもそも、そのフックがどのクエリにかかっているかを絞る
あなたのプラグインに、$query->get('meta_query') に [] で足している行はありませんか。
手元で試すコマンド
WordPress の直下に置いて php mq.php で動きます。投稿タイプ mqtest の投稿を6件作り、最後に消します。
<?php
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_URI'] = '/';
require __DIR__ . '/wp-load.php';
$ids = [];
foreach ([['red', 0], ['blue', 0], ['green', 0], ['red', 1], ['blue', 1], ['green', 1]] as [$c, $h]) {
$id = wp_insert_post(['post_title' => $c . ($h ? '(生成画像)' : ''), 'post_status' => 'publish', 'post_type' => 'mqtest']);
update_post_meta($id, 'color', $c);
if ($h) {
update_post_meta($id, '_rapls_pic_is_thumbnail', '1');
}
$ids[] = $id;
}
$hide = ['relation' => 'OR',
['key' => '_rapls_pic_is_thumbnail', 'compare' => 'NOT EXISTS'],
['key' => '_rapls_pic_is_thumbnail', 'value' => '1', 'compare' => '!=']];
$wrap = fn($mq) => (!is_array($mq) || $mq === []) ? [$hide] : ['relation' => 'AND', $mq, $hide];
$run = function ($label, $mq) {
$q = new WP_Query(['post_type' => 'mqtest', 'posts_per_page' => -1, 'meta_query' => $mq,
'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids']);
printf("%s %d件: %s\n", $label, count($q->posts), implode(', ', array_map('get_the_title', $q->posts)));
};
$or = ['relation' => 'OR', ['key' => 'color', 'value' => 'red'], ['key' => 'color', 'value' => 'blue']];
$run('A 元の条件だけ', $or);
$b = $or; $b[] = $hide;
$run('B 末尾に足す', $b);
$run('C AND で包む', $wrap($or));
foreach ($ids as $i) {
wp_delete_post($i, true);
}
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/