过滤
集合(Collections) 是资源的列表。在解耦站点中,你会用它们在客户端创建诸如“新内容(New Content)”列表或“我的内容(My content)”版块之类的东西。
然而,当你对诸如 /jsonapi/node/article
这样的集合端点发起未加筛选的请求时,你只会得到你被允许查看的所有文章。
没有筛选器(filters),你就无法只获取你的文章,或只获取关于羊驼的文章。
本指南将教你如何像专业人士一样构建筛选器。
快速开始
最简单、最常见的筛选器是键值筛选(key-value filter):
?filter[field_name]=value&filter[field_other]=value
这会匹配所有“field_name”等于“value”且“field_other”等于“value”的资源。
想了解更多,请继续往下看!
概要
JSON:API 模块拥有业内最健壮、功能最丰富的筛选能力之一。不过,强大也意味着需要一点学习曲线。
在读完本文后,你将能够构建复杂查询,并能推理你可能遇到的问题,例如“如何获取某作者的关于羊驼 或 动物王国速度最快的成员——游隼(peregrine falcon)——的文章列表?”
我们将从最基础的内容开始。之后,我们会展示一些让编写筛选更快、更不啰嗦的小捷径。最后,我们会看到许多来自真实场景的筛选示例。
如果你不是 Drupal 新手,你很可能已经用过 Views 模块来做这类事情。与 Drupal Core 自带的 REST 模块不同,JSON:API 不会导出 Views 的结果。 在 JSON:API 中,集合是对 Views 中导出的“REST 显示(REST displays)”的 API-First 替代方案。
构建筛选器
JSON:API 筛选器的基本构件是 条件(conditions) 和 分组(groups)。条件断言某件事为真;分组允许你把这些断言组合成逻辑集合,从而形成更大的条件组。这些集合可以嵌套,以构建非常精细的查询。你可以把这些嵌套集合想象成一棵树:
常规表示法:
a( b() && c( d() || e() ) )
树形表示法:
a
/ \
b & c
/ \
d | e
在两种表示中:
“d”和“e”是“c”的成员,处于 OR 分组。
“b”和“c”是“a”的成员,处于 AND 分组。
那么,一个条件内部包含什么?
让我们来点逻辑 🖖。记住,条件告诉你关于某个资源及其某个断言的真(TRUE)或假(FALSE)的判断,比如“该实体是否由某个特定用户创建?” 当条件对某个资源为 FALSE 时,该资源就不会包含在集合中。
一个条件有三个主要部分:路径(path)、操作符(operator)和值(value)。
- “path”用于标识资源上的某个字段
- “operator”是比较的方法
- “value”是你用于比较的对象
用伪代码表示,一个条件看起来像这样:
($field !== 'space')
其中:
$field
是由“path”标识的资源字段- “operator”是
!==
- “value”是字符串
'space'
在 JSON:API 模块中,我们无法把它写得像上面那么漂亮,因为我们需要让它在 URL 查询字符串里工作。为此,我们用键/值对来表示每个条件。
如果我们要筛选用户的名(first name),一个条件可能会是这样的:
?filter[a-label][condition][path]=field_first_name
&filter[a-label][condition][operator]=%3D <- 编码后的“=”符号
&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 <- 已编码的“=”
&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()
函数获得正确的编码。
条件分组(Condition Groups)
现在我们已经知道如何构建条件,但还不知道如何构建条件的分组。我们如何像上面的树那样进行构建?
为此,我们需要一个“分组(group)”。分组是由一个“连接词(conjunction)”连接起来的一组条件。所有分组都有连接词,连接词要么是 AND,要么是 OR。
现在我们的筛选有点儿过于具体了!假设我们想找到所有姓以 “J” 开头,且名为 “Janis” 或 名为 “Joan” 的用户。
要实现这一点,我们新增一个分组:
?filter[rock-group][group][conjunction]=OR
然后,我们需要把上面的筛选分配到这个新分组中。
为此,我们增加一个 memberOf
键。每个条件和分组都可以拥有一个 memberOf
键。
提示:分组和条件一样也可以有 memberOf
键,这意味着我们可以有“分组的分组” 🤯!
注意:每个没有 memberOf
键的筛选项都被认为属于一个“根”分组(root group),其连接词为 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(根 AND 分组)
/ \
/ \ b = last-name-filter
b c c = rock-group
/ \
/ \ d = janis-filter
d e e = joan-filter
你可以按照心意将这些分组进行任意深度的嵌套。
路径(Paths)
条件还有一个最后的特性:“路径(paths)”
路径提供了一种基于关系(relationship)值进行筛选的方式。
到目前为止,我们只是基于用户资源上的假想字段 field_first_name
和 field_last_name
进行筛选。
设想我们想根据用户的职业名称来筛选,而职业类型被存储为一个独立的资源。我们可以这样添加筛选:
?filter[career][condition][path]=field_career.name
&filter[career][condition][operator]=%3D
&filter[career][condition][value]=Rockstar
路径使用“点号(dot)表示法”来穿越关系。
如果一个资源有某个关系字段,你可以通过将该关系字段名与该关系所指资源的字段名用 .
(点)连接起来,来对其添加筛选。
你甚至可以根据“关系的关系”(依此类推)进行筛选,只需要继续添加字段名与点即可。
提示:你可以通过在路径中放入一个非负整数来筛选关系中的某个特定索引。例如路径 some_relationship.1.some_attribute
只会根据第二个相关资源进行筛选。
提示:你可以对字段的子属性进行筛选。例如,即使 field_phone
不是关系字段,像 field_phone.country_code
这样的路径也能正常工作。
提示:当针对配置属性进行筛选时,你可以使用星号(*)来充当路径中任意部分的通配符。例如,/jsonapi/field_config/field_config?filter[dependencies.config.*]=comment.type.comment
会匹配所有在 ["attributes"]["dependencies"]["config"]
(一个索引数组)中包含值“comment.type.comment”的字段配置。
捷径(Shortcuts)
这需要输入很多字符。多数时候,你并不需要如此复杂的筛选;对于这些场景,JSON:API 模块提供了一些“捷径”帮助你更快地编写筛选。
当操作符是 =
时,你可以不写它。系统会自动假定。因此:
?filter[a-label][condition][path]=field_first_name
&filter[a-label][condition][operator]=%3D <- 编码后的“=”符号
&filter[a-label][condition][value]=Janis
变为
?filter[janis-filter][condition][path]=field_first_name
&filter[janis-filter][condition][value]=Janis
此外,你很少需要对同一个字段进行两次筛选(尽管理论上可行)。因此,当操作符为 =
且你不需要对同一字段筛选两次时,路径本身就可以作为标识符。于是:
?filter[janis-filter][condition][path]=field_first_name
&filter[janis-filter][condition][value]=Janis
变为
?filter[field_first_name][value]=Janis
那个额外的 value
也挺繁琐。这就是为什么你可以将最简单的等值判断简化成键值形式:
?filter[field_first_name]=Janis
筛选与访问控制(Access Control)
首先要提醒:不要把筛选与访问控制混为一谈。仅仅因为你编写了一个筛选把用户不应看到的东西过滤掉,并不意味着它就不可访问。务必在后端执行访问检查。
有了这个重要的前提,我们来谈谈如何用筛选来补充访问控制。为了提升性能,你应该预先筛除用户不会看到的内容。JSON:API 问题队列里最常见的一类支持请求,往往可以用这个简单技巧解决!
如果你知道你的用户看不到未发布(unpublished)的内容,添加如下筛选:
?filter[status][value]=1
通过这种方式,你会减少不必要的请求数量。这是因为,对于用户无权访问的资源,JSON:API 不会返回数据。 你可以检查 JSON:API 文档中的 meta.errors
部分,查看哪些资源可能受到了影响。
因此,尽可能提前筛除不可访问的资源。
筛选示例
1. 只获取已发布的节点(nodes)
一个非常常见的场景是仅加载已发布的节点。这个筛选非常简单:
简写
filter[status][value]=1
常规
filter[status-filter][condition][path]=status
filter[status-filter][condition][value]=1
2. 通过实体引用(entity reference)的值获取节点
一种常见策略是通过实体引用来筛选内容。
简写
filter[uid.id][value]=BB09E2CD-9487-44BC-B219-3DC03D6820CD
常规
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 起,也可以按 target_id
进行筛选,而不仅仅是按 uuid
属性。
简写
filter[field_tags.meta.drupal_internal__target_id]=1
常规
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
是用户实体的一个字段。
简写
filter[uid.name][value]=admin
常规
filter[name-filter][condition][path]=uid.name
filter[name-filter][condition][value]=admin
4. 使用数组筛选:获取由用户 [admin, john] 创建的节点
你可以为筛选提供多个值进行检索。除字段与值键外,你还可以为条件添加操作符。通常是 “=”,但也可以使用 “IN”、“NOT IN”、“>”、“<”、“<>”、“BETWEEN”。
在此示例中我们将使用 IN 操作符。注意我在 value 后添加了两组方括号,将其变为数组。
常规
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 客户端只会创建一个值,因为数组键相同会覆盖之前的值。最好使用带索引的写法以创建唯一的数组元素。
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. 分组中的分组:获取被置顶(sticky)或已推荐(promoted)且由 admin 创建的节点
如在“分组”章节所述,你可以把分组放进另外的分组。
WHERE (user.name = admin) AND (node.sticky = 1 OR node.promoted = 1)
为此,我们将 sticky 和 promoted 放进一个连接词为 OR 的分组。再创建一个连接词为 AND 的分组,并把 admin 的筛选以及前述 OR 分组放入其中。
# 创建 AND 与 OR 分组
filter[and-group][group][conjunction]=AND
filter[or-group][group][conjunction]=OR
# 把 OR 分组放入 AND 分组
filter[or-group][group][memberOf]=and-group
# 创建 admin 筛选并放入 AND 分组
filter[admin-filter][condition][path]=uid.name
filter[admin-filter][condition][value]=admin
filter[admin-filter][condition][memberOf]=and-group
# 创建 sticky 筛选并放入 OR 分组
filter[sticky-filter][condition][path]=sticky
filter[sticky-filter][condition][value]=1
filter[sticky-filter][condition][memberOf]=or-group
# 创建 promoted 筛选并放入 OR 分组
filter[promote-filter][condition][path]=promote
filter[promote-filter][condition][value]=1
filter[promote-filter][condition][memberOf]=or-group
7. 筛选标题(title)包含 “Foo” 的节点
简写
filter[title][operator]=CONTAINS&filter[title][value]=Foo
常规
filter[title-filter][condition][path]=title
filter[title-filter][condition][operator]=CONTAINS
filter[title-filter][condition][value]=Foo
8. 按非标准复杂字段(例如 addressfield)筛选
按城市(locality)筛选
filter[field_address][condition][path]=field_address.locality
filter[field_address][condition][value]=Mordor
按地址行(address line)筛选
filter[address][condition][path]=field_address.address_line1
filter[address][condition][value]=Rings Street
9. 按分类术语(Taxonomy term)值(如 tags)进行筛选
进行筛选时,你需要使用词汇表(vocabulary)的机读名(machine name)以及节点上存在的相应字段。
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
注意,时间戳字段(如 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(不带 value):
filter[my-filter][condition][path]=my_field
filter[my-filter][condition][operator]=IS NULL
文章来自 Drupal 文档。