フィルタリング
コレクション はリソースの一覧です。デカップルド(分離)サイトでは、クライアント側で「新着コンテンツ」リストや「マイコンテンツ」セクションなどを作成するために使用するものです。
ただし、 /jsonapi/node/article のようなコレクションエンドポイントにフィルターなしでリクエストすると、閲覧を許可されているすべての 記事が返されます。
フィルターがないと、自分の 記事だけや、リャマについての 記事だけを取得することはできません。
このガイドでは、プロのようにフィルターを構築する方法を説明します。
クイックスタート
最もシンプルで一般的なフィルターは、キーと値のフィルターです:
?filter[field_name]=value&filter[field_other]=value
これは、「field_name」が「value」に等しく、「field_other」が「value」に等しいすべてのリソースに一致します。
それ以外のことは、読み進めてください!
概要
JSON:APIモジュールには、最も堅牢で機能豊富なフィルタリング機能が備わっています。ただし、その強力さの分、多少の学習曲線があります。
この記事を読み終える頃には、複雑なクエリを作成し、「著者の記事の中で、リャマについての記事または 世界最速の動物である ハヤブサ についての記事の一覧を取得するにはどうすればよいか?」のような、直面するであろう問題について考えることができるようになります。
まず基本的なことから順に説明していきます。その後、フィルターの記述を少し速く簡潔にするためのショートカットをいくつか紹介します。最後に、実際の事例から取ったフィルターの例をいくつか見ていきます。
Drupalに詳しい方なら、おそらくこういった用途にすでにViewsモジュールを使用したことがあるでしょう。 Drupal Coreに付属するRESTモジュールとは異なり、JSON:APIはViewsの結果をエクスポートしません。 コレクションは、Viewsでエクスポートされた「RESTディスプレイ」に代わる、JSON:APIのAPIファーストな代替手段です。
フィルターの構築
JSON:APIのフィルターの基本的な構成要素は、 条件(condition) と グループ(group) です。条件は「何かが真である」ことを表明し、グループはそれらの表明を論理的な集合にまとめて、より大きな条件グループを作ることを可能にします。それらの集合はネストして、極めて細かいクエリを作ることができます。ネストされた集合は、次のように木構造と考えることができます:
Conventional representation:
a( b() && c( d() || e() ) )
Tree representation:
a
/ \
b & c
/ \
d | e
In both representations:
"d" and "e" are members of "c" in an OR group.
"b" and "c" are members of "a" in an AND group.
では、条件の中身は何でしょうか?
論理的に考えましょう ??. 覚えておいてください。条件とは、リソースと、それについて作成する表明(例:「このエンティティは特定のユーザーによって作成されたか?」)について、TRUEかFALSEかを示すものです。あるリソースについて条件がFALSEの場合、そのリソースはコレクションに含まれません。
条件には、パス(path)、演算子(operator)、値(value)の3つの主要な部分があります。
- 「パス」はリソース上のフィールドを識別します
- 「演算子」は比較の方法です
- 「値」は比較の対象となるものです
擬似コードでは、条件は次のようなものになります:
($field !== 'space')
ここで:
$fieldは「パス」で識別されたリソースのフィールドです- 「演算子」は
!==です - 「値」は文字列
'space'です
JSON:APIモジュールでは、URLクエリ文字列内で動作させる必要があるため、それほどきれいな形にはできません。そのため、 各条件をキーと値のペアで表現します。
ユーザーの名でフィルタリングする場合、条件は次のようになります:
?filter[a-label][condition][path]=field_first_name
&filter[a-label][condition][operator]=%3D <- encoded "=" symbol
&filter[a-label][condition][value]=Janis
最初の角括弧の中にラベルを入れたことに注目してください。 b-label、 this_is_my_super_awesome_label、あるいは 666 のような整数 ???? にすることも簡単にできました。重要なのは、 すべての条件とグループには識別子が必要であるということです。
しかし、システム内にたくさんの 「Janis」がいたらどうでしょう?
姓が「J」で始まるJanisだけを取得するために、別のフィルターを追加しましょう:
?filter[first-name-filter][condition][path]=field_first_name
&filter[first-name-filter][condition][operator]=%3D <- encoded "="
&filter[first-name-filter][condition][value]=Janis
&filter[last-name-filter][condition][path]=field_last_name
&filter[last-name-filter][condition][operator]=STARTS_WITH
&filter[last-name-filter][condition][value]=J
Janisの複数形は「Janii」かもしれません ??...
フィルター演算子は = や STARTS_WITH だけではありません。JSON:APIのコードベースからそのまま抜粋した完全なリストがこちらです:
\Drupal\jsonapi\Query\EntityCondition::$allowedOperators = [
'=', '<>',
'>', '>=', '<', '<=',
'STARTS_WITH', 'CONTAINS', 'ENDS_WITH',
'IN', 'NOT IN',
'BETWEEN', 'NOT BETWEEN',
'IS NULL', 'IS NOT NULL',
];
記号の演算子はURLエンコードする必要があります。正しいエンコードはPHPの urlencode() 関数で取得できます。
条件グループ
条件の構築方法はわかりましたが、 条件のグループ の構築方法はまだわかりません。先ほど見たような木構造はどうやって作るのでしょうか?
そのためには「グループ」が必要です。グループとは、「結合(conjunction)」で結ばれた条件の集合です。 すべてのグループには結合があり、結合はAND(論理積)またはOR(論理和)のいずれかです。
さて、今度はフィルターが少し細かすぎる としましょう!姓が「J」で始まり、名が「Janis」または 「Joan」であるすべてのユーザーを検索したいとします。
そのために、グループを追加します:
?filter[rock-group][group][conjunction]=OR
次に、新しいグループにフィルターを割り当てる必要があります。
そのために、 memberOf キーを追加します。 すべての条件とグループは memberOf キーを持つことができます。
ヒント:グループは条件と同じように memberOf キーを持つことができます。つまり、グループのグループを持つことができます ??!
注: memberOf キーを持たないすべてのフィルターは、結合がANDの「ルート」グループの一部とみなされます。
すべてまとめると:
?filter[rock-group][group][conjunction]=OR
&filter[janis-filter][condition][path]=field_first_name
&filter[janis-filter][condition][operator]=%3D
&filter[janis-filter][condition][value]=Janis
&filter[janis-filter][condition][memberOf]=rock-group
&filter[joan-filter][condition][path]=field_first_name
&filter[joan-filter][condition][operator]=%3D
&filter[joan-filter][condition][value]=Joan
&filter[joan-filter][condition][memberOf]=rock-group
&filter[last-name-filter][condition][path]=field_last_name
&filter[last-name-filter][condition][operator]=STARTS_WITH
&filter[last-name-filter][condition][value]=J
見覚えがありますか?
当然でしょう。上で木構造として見たものです:
a a = root-and-group
/ \
/ \ b = last-name-filter
b c c = rock-group
/ \
/ \ d = janis-filter
d e e = joan-filter
これらのグループは、好きなだけ深くネストできます。
パス
条件にはもう1つの機能があります。「パス」です。
パスは、リレーションシップの値に基づいてフィルタリングする方法を提供します。
これまでのところ、仮想的なユーザーリソースの field_first_name と field_last_name でフィルタリングしてきました。
キャリアの種類が別のリソースとして保存されている場合、ユーザーのキャリア名でフィルタリングしたいと想像してください。次のようなフィルターを追加できます:
?filter[career][condition][path]=field_career.name
&filter[career][condition][operator]=%3D
&filter[career][condition][value]=Rockstar
パスは「ドット記法」を使用してリレーションシップをたどります。
リソースにリレーションシップがある場合、リレーションシップのフィールド名と リレーションシップ側の フィールド名を . (ドット)で連結することで、それに対するフィルターを追加できます。
フィールド名とドットを追加するだけで、リレーションシップのリレーションシップ(さらにその先も)でフィルタリングすることもできます。
ヒント:パスに非負の整数を入れることで、リレーションシップの特定のインデックスでフィルタリングできます。つまり、パス some_relationship.1.some_attribute は2番目の関連リソースのみでフィルタリングします。
ヒント:フィールドのサブプロパティでもフィルタリングできます。例えば、 field_phone がリレーションシップでなくても、 field_phone.country_code のようなパスは機能します。
ヒント:設定プロパティに対してフィルタリングする場合、アスタリスク(*)をパスの任意の部分の代替として使用できます。例えば、 /jsonapi/field_config/field_config?filter[dependencies.config.*]=comment.type.comment は、 ["attributes"]["dependencies"]["config"] (インデックス配列)に「comment.type.comment」という値を含むすべてのフィールド設定に一致します。
ショートカット
入力する文字数が多いですね。ほとんどの場合、そのような複雑なフィルターは必要ありません。そのようなケースのために、JSON:APIモジュールにはフィルターをより速く記述するための「ショートカット」がいくつか用意されています。
演算子が = の場合、それを含める必要はありません。暗黙的に想定されます。つまり:
?filter[a-label][condition][path]=field_first_name
&filter[a-label][condition][operator]=%3D <- encoded "=" symbol
&filter[a-label][condition][value]=Janis
becomes
?filter[janis-filter][condition][path]=field_first_name
&filter[janis-filter][condition][value]=Janis
同じフィールドで2回フィルタリングする必要があるケースも稀です(不可能ではありませんが)。そこで、演算子が = で、同じフィールドで2回フィルタリングする必要がない場合、パスを識別子にできます。つまり:
?filter[janis-filter][condition][path]=field_first_name
&filter[janis-filter][condition][value]=Janis
becomes
?filter[field_first_name][value]=Janis
余計な value が厄介ですね。そのため、最もシンプルな等価チェックはキーと値の形式に簡略化できます:
?filter[field_first_name]=Janis
フィルターとアクセス制御
まず、警告です。フィルターをアクセス制御と混同しないようにしてください。ユーザーが見るべきでないものを除外するフィルターを書いたからといって、それがアクセスできないわけではありません。 常にバックエンドでアクセスチェックを実行してください。
その大きな注意点を踏まえた上で、アクセス制御を補完するためのフィルターの使い方について説明します。パフォーマンスを向上させるには、ユーザーが見ることができない ものをフィルターで除外する必要があります。JSON:APIのイシューキューで最も多いサポート依頼は、このシンプルなテクニック1つで解決できます!
ユーザーが未公開コンテンツを見られないことがわかっている場合は、次のフィルターを追加してください:
?filter[status][value]=1
この方法を使えば、不要なリクエストの数を減らせます。これは、 JSON:APIはユーザーがアクセス権を持たないリソースのデータを返さないため です。どのリソースが影響を受けた可能性があるかは、JSON:APIドキュメントの meta.errors セクションを確認することでわかります。
つまり、 アクセスできないリソースを事前にフィルターで除外することをお勧めします。
フィルターの例
1. 公開済みノードだけを取得する
非常によくあるシナリオとして、公開済みのノードだけを読み込むというものがあります。これは追加が非常に簡単なフィルターです。
SHORT
filter[status][value]=1
NORMAL
filter[status-filter][condition][path]=status
filter[status-filter][condition][value]=1
2. エンティティ参照の値でノードを取得する
コンテンツをエンティティ参照でフィルタリングするのは、一般的な戦略です。
SHORT
filter[uid.id][value]=BB09E2CD-9487-44BC-B219-3DC03D6820CD
NORMAL
filter[author-filter][condition][path]=uid.id
filter[author-filter][condition][value]=BB09E2CD-9487-44BC-B219-3DC03D6820CD
JSON:API仕様に完全に準拠するため、Drupalは内部的に uuid プロパティを使用しますが、JSON:APIは代わりに id を使用します。
Drupal 9.3以降は、 uuid プロパティだけではなく target_id でもフィルタリングが可能です。
SHORT
filter[field_tags.meta.drupal_internal__target_id]=1
NORMAL
filter[name-filter][condition][path]=field_tags.meta.drupal_internal__target_id
filter[name-filter][condition][value]=1
3. ネストされたフィルター:ユーザーadminによって作成されたノードを取得する
ユーザー、タクソノミーフィールド、その他のエンティティ参照フィールドなど、参照されたエンティティのフィールドでフィルタリングできます。次のような表記(reference_field.nested_field)を使うだけで簡単にできます。この例では、参照フィールドはユーザーのuidで、nameはユーザーエンティティのフィールドです。
SHORT
filter[uid.name][value]=admin
NORMAL
filter[name-filter][condition][path]=uid.name
filter[name-filter][condition][value]=admin
4. 配列を使ったフィルタリング:ユーザー[admin, john]によって作成されたノードを取得する。
フィルターに検索対象の複数の値を指定できます。フィールドと値のキーに加えて、条件に演算子を追加できます。通常は「=」ですが、「IN」「NOT IN」「>」「<」「<>」「BETWEEN」も使用できます。
この例ではIN演算子を使用します。値を配列にするために、valueの後ろに角括弧を2つ追加したことに注目してください。
NORMAL
filter[name-filter][condition][path]=uid.name
filter[name-filter][condition][operator]=IN
filter[name-filter][condition][value][1]=admin
filter[name-filter][condition][value][2]=john
ヒント:複数値フィルターで角括弧を使用する場合、新しい値に空の角括弧を使用するだけではいけません。
これらはURLに入力すると機能しますが、Guzzleやその他のHTTPクライアントでは、配列キーが同じとみなされて前の値を上書きするため、1つの値しか作成されません。一意の配列要素を作成するにはインデックスを使用することをお勧めします。
5. フィルターのグループ化:公開済みかつadminによって作成されたノードを取得する。
では、上記の例のいくつかを組み合わせて、次のシナリオを作成しましょう。
WHERE user.name = admin AND node.status = 1;
filter[and-group][group][conjunction]=AND
filter[name-filter][condition][path]=uid.name
filter[name-filter][condition][value]=admin
filter[name-filter][condition][memberOf]=and-group
filter[status-filter][condition][path]=status
filter[status-filter][condition][value]=1
filter[status-filter][condition][memberOf]=and-group
and-groupを追加する必要は必ずしもありませんが、通常はその方が少し簡単だと感じます。
6. グループ化されたフィルターのグループ化:昇格(promoted)または固定(sticky)され、かつadminによって作成されたノードを取得する
グループ化のセクションで述べたように、グループを他のグループの中に入れることができます。
WHERE (user.name = admin) AND (node.sticky = 1 OR node.promoted = 1)
これを行うには、stickyとpromotedを結合ORのグループに入れます。結合ANDのグループを作成し、その中に
adminフィルターとpromoted/stickyのORグループを入れます。
# Create an AND and an OR GROUP
filter[and-group][group][conjunction]=AND
filter[or-group][group][conjunction]=OR
# Put the OR group into the AND GROUP
filter[or-group][group][memberOf]=and-group
# Create the admin filter and put it in the AND GROUP
filter[admin-filter][condition][path]=uid.name
filter[admin-filter][condition][value]=admin
filter[admin-filter][condition][memberOf]=and-group
# Create the sticky filter and put it in the OR GROUP
filter[sticky-filter][condition][path]=sticky
filter[sticky-filter][condition][value]=1
filter[sticky-filter][condition][memberOf]=or-group
# Create the promoted filter and put it in the OR GROUP
filter[promote-filter][condition][path]=promote
filter[promote-filter][condition][value]=1
filter[promote-filter][condition][memberOf]=or-group
7. タイトルに「Foo」が含まれる(CONTAINS)ノードのフィルター
SHORT
filter[title][operator]=CONTAINS&filter[title][value]=Foo
NORMAL
filter[title-filter][condition][path]=title
filter[title-filter][condition][operator]=CONTAINS
filter[title-filter][condition][value]=Foo
8. 非標準の複雑なフィールド(例:addressfield)でフィルタリングする
FILTER BY LOCALITY
filter[field_address][condition][path]=field_address.locality
filter[field_address][condition][value]=Mordor
FILTER BY ADDRESS LINE
filter[address][condition][path]=field_address.address_line1
filter[address][condition][value]=Rings Street
9. タクソノミータームの値(例:タグ)でフィルタリングする
フィルタリングには、ボキャブラリのマシン名と、ノードに存在するフィールドを使用する必要があります。
filter[taxonomy_term--tags][condition][path]=field_tags.name
filter[taxonomy_term--tags][condition][operator]=IN
filter[taxonomy_term--tags][condition][value][]=tagname
10. 日付(日付のみ、時間なし)でフィルタリングする
日付はフィルタリング可能です。 ISO-8601形式に準拠した時刻文字列を渡してください。
この例は、日付のみ(時間なし)に設定されたDateフィールド用です。
filter[datefilter][condition][path]=field_test_date
filter[datefilter][condition][operator]=%3D
filter[datefilter][condition][value]=2019-06-27
この例は、日付と時刻をサポートするDateフィールド用です。
filter[datefilter][condition][path]=field_test_date
filter[datefilter][condition][operator]=%3D
filter[datefilter][condition][value]=2019-06-27T16%3A00%3A00
timestampフィールド(createdや changedなど)は、現在はフィルタリングにタイムスタンプを使用する必要があることに注意してください:
filter[recent][condition][path]=created
filter[recent][condition][operator]=%3D
filter[recent][condition][value]=1591627496
11. 空の配列フィールドでフィルタリングする
この例は、値が選択されていないチェックボックス/ラジオボタンのフィールド用です。チェックボックスのフィールドがあるとします。その値がチェックされていないすべてのノードを取得したいとします。チェックされている場合、JSON:APIは配列を返します:
"my_field":["checked"]
チェックされていない場合、JSON:APIは空の配列を返します:
"my_field": []
チェックされていないすべてのフィールドを取得するには、次のように配列に対してIS NULLを使用する必要があります(値を指定しません):
filter[my-filter][condition][path]=my_field
filter[my-filter][condition][operator]=IS NULL
記事:Drupal ドキュメンテーションより。