动态值的 Postgres jsonb 查询
Postgres jsonb query for dynamic values
在用户 table 中,我有一个 jsob 列 experience
具有以下 json 结构:
[
{
"field": "devops",
"years": 9
},
{
"field": "backend dev",
"years": 7
}
... // could be N number of objects with different values
]
Business requirement
客户可以要求在任何领域都有经验并且在每个领域都有各自年限的人
This is an example query
SELECT * FROM users
WHERE
jsonb_path_exists(experience, '$[*] ? (@.field == "devops" && @.years > 5)') and
jsonb_path_exists(experience, '$[*] ? (@.field == "backend dev" && @.years > 5)')
LIMIT 3;
问题
假设我收到
的请求
[
{ field: "devops", years: 5 },
{ field: "java", years: 6 },
{ field: "ui/ux", years: 2 }] // and so on
如何动态创建查询而不用担心 sql 注入?
技术栈
- 节点
- 打字稿
- 类型ORM
- Postgres
这是一个参数化查询,因此或多或少是注入安全的。 qualifies
标量子查询计算是否experience
满足所有请求项。参数为</code>(请求参数的jsonb数组)和<code>
(限制值)。您可能需要根据您的环境改变它们的语法。
select t.* from
(
select u.*,
(
select count(*) = jsonb_array_length()
from jsonb_array_elements(u.experience) ej -- jsonb list of experiences
inner join jsonb_array_elements() rj -- jsonb list of request items
on ej ->> 'field' = rj ->> 'field'
and (ej ->> 'years')::numeric >= (rj ->> 'years')::numeric
) as qualifies
from users as u
) as t
where t.qualifies
limit ;
一些解释
qualifies
子查询的逻辑是这样的:首先'normalize'将experience
请求jsonb数组放入'tables',然后在目标条件上内联(在这种情况下是 field_a = field_b and years_a >= years_b
)并计算其中有多少匹配。如果计数等于请求项的数量(即 count(*) = jsonb_array_length()
),则所有项都满足,因此 experience
符合条件。
因此不需要动态 SQL 。我认为这种方法也可以重复使用。
索引
首先,您需要索引支持。我建议使用 jsonb_path_ops
索引,例如:
CREATE INDEX users_experience_gin_idx ON users USING gin (experience jsonb_path_ops);
参见:
- Index for finding an element in a JSON array
查询
还有一个 查询 可以利用该索引(100 % 等同于您的原始索引):
SELECT *
FROM users
WHERE experience @? '$[*] ? (@.field == "devops" && @.years > 5 )'
AND experience @? '$[*] ? (@.field == "backend dev" && @.years > 5)'
LIMIT 3;
需要 Postgres 12 或更高版本,其中添加了 SQL/JSON 路径语言。
索引支持绑定到 Postgres 中的 operators。 operator @?
相当于 jsonb_path_exists()
。参见:
动态生成查询
SELECT 'SELECT * FROM users
WHERE experience @? '
|| string_agg(quote_nullable(format('$[*] ? (@.field == %s && @.years > %s)'
, f->'field'
, f->'years')) || '::jsonpath'
, E'\nAND experience @? ')
|| E'\nLIMIT 3'
FROM jsonb_array_elements('[{"field": "devops", "years": 5 },
{"field": "java", "years": 6 },
{"field": "ui/ux", "years": 2 }]') f;
生成上述形式的查询:
SELECT * FROM users
WHERE experience @? '$[*] ? (@.field == "devops" && @.years > 5)'::jsonpath
AND experience @? '$[*] ? (@.field == "java" && @.years > 6)'::jsonpath
AND experience @? '$[*] ? (@.field == "ui/ux" && @.years > 2)'::jsonpath
LIMIT 3;
全自动化
How do I dynamically create a query without worrying about sql injection?
把上面的查询生成放到一个PL/pgSQL函数中动态执行:
CREATE OR REPLACE FUNCTION f_users_with_experience(_filter_arr jsonb, _limit int = 3)
RETURNS SETOF users
LANGUAGE plpgsql PARALLEL SAFE STABLE STRICT AS
$func$
DECLARE
_sql text;
BEGIN
-- assert (you may want to be stricter?)
IF jsonb_path_exists (_filter_arr, '$[*] ? (!exists(@.field) || !exists(@.years))') THEN
RAISE EXCEPTION 'Parameter (_filter_arr) must be a JSON array with keys "field" and "years" in every object. Invalid input was: >>%<<', _filter_arr;
END IF;
-- generate query string
SELECT INTO _sql
'SELECT * FROM users
WHERE experience @? '
|| string_agg(quote_nullable(format('$[*] ? (@.field == %s && @.years > %s)'
, f->'field'
, f->'years'))
, E'\nAND experience @? ')
|| E'\nLIMIT ' || _limit
FROM jsonb_array_elements(_filter_arr) f;
-- execute
IF _sql IS NULL THEN
RAISE EXCEPTION 'SQL statement is NULL. Should not occur!';
ELSE
-- RAISE NOTICE '%', _sql; -- debug first if in doubt
RETURN QUERY EXECUTE _sql;
END IF;
END
$func$;
致电:
SELECT * FROM f_users_with_experience('[{"field": "devops", "years": 5 },
, {"field": "backend dev", "years": 6}]');
或者用不同的 LIMIT
:
SELECT * FROM f_users_with_experience('[{"field": "devops", "years": 5 }]', 123);
db<>fiddle here
您应该熟悉 PL/pgSQL 才能使用并理解它。
SQL注入是不可能的因为...
- 有效JSON 输入被强制执行
- JSON 值与原始 JSON 双引号连接。
- 最重要的是,每个生成的
jsonpath
值都用 quote_nullable()
单引号括起来。
在讨论 SQL/JSON 路径表达式时,我使用一个来断言有效输入:
jsonb_path_exists (_filter_arr, '$[*] ? (!exists(@.field) || !exists(@.years))')
检查 JSON 数组中的每个对象,以及是否缺少两个必需键(field
、years
)之一。
在用户 table 中,我有一个 jsob 列 experience
具有以下 json 结构:
[
{
"field": "devops",
"years": 9
},
{
"field": "backend dev",
"years": 7
}
... // could be N number of objects with different values
]
Business requirement
客户可以要求在任何领域都有经验并且在每个领域都有各自年限的人
This is an example query
SELECT * FROM users
WHERE
jsonb_path_exists(experience, '$[*] ? (@.field == "devops" && @.years > 5)') and
jsonb_path_exists(experience, '$[*] ? (@.field == "backend dev" && @.years > 5)')
LIMIT 3;
问题
假设我收到
的请求[
{ field: "devops", years: 5 },
{ field: "java", years: 6 },
{ field: "ui/ux", years: 2 }] // and so on
如何动态创建查询而不用担心 sql 注入?
技术栈
- 节点
- 打字稿
- 类型ORM
- Postgres
这是一个参数化查询,因此或多或少是注入安全的。 qualifies
标量子查询计算是否experience
满足所有请求项。参数为</code>(请求参数的jsonb数组)和<code>
(限制值)。您可能需要根据您的环境改变它们的语法。
select t.* from
(
select u.*,
(
select count(*) = jsonb_array_length()
from jsonb_array_elements(u.experience) ej -- jsonb list of experiences
inner join jsonb_array_elements() rj -- jsonb list of request items
on ej ->> 'field' = rj ->> 'field'
and (ej ->> 'years')::numeric >= (rj ->> 'years')::numeric
) as qualifies
from users as u
) as t
where t.qualifies
limit ;
一些解释
qualifies
子查询的逻辑是这样的:首先'normalize'将experience
请求jsonb数组放入'tables',然后在目标条件上内联(在这种情况下是 field_a = field_b and years_a >= years_b
)并计算其中有多少匹配。如果计数等于请求项的数量(即 count(*) = jsonb_array_length()
),则所有项都满足,因此 experience
符合条件。
因此不需要动态 SQL 。我认为这种方法也可以重复使用。
索引
首先,您需要索引支持。我建议使用 jsonb_path_ops
索引,例如:
CREATE INDEX users_experience_gin_idx ON users USING gin (experience jsonb_path_ops);
参见:
- Index for finding an element in a JSON array
查询
还有一个 查询 可以利用该索引(100 % 等同于您的原始索引):
SELECT *
FROM users
WHERE experience @? '$[*] ? (@.field == "devops" && @.years > 5 )'
AND experience @? '$[*] ? (@.field == "backend dev" && @.years > 5)'
LIMIT 3;
需要 Postgres 12 或更高版本,其中添加了 SQL/JSON 路径语言。
索引支持绑定到 Postgres 中的 operators。 operator @?
相当于 jsonb_path_exists()
。参见:
动态生成查询
SELECT 'SELECT * FROM users
WHERE experience @? '
|| string_agg(quote_nullable(format('$[*] ? (@.field == %s && @.years > %s)'
, f->'field'
, f->'years')) || '::jsonpath'
, E'\nAND experience @? ')
|| E'\nLIMIT 3'
FROM jsonb_array_elements('[{"field": "devops", "years": 5 },
{"field": "java", "years": 6 },
{"field": "ui/ux", "years": 2 }]') f;
生成上述形式的查询:
SELECT * FROM users
WHERE experience @? '$[*] ? (@.field == "devops" && @.years > 5)'::jsonpath
AND experience @? '$[*] ? (@.field == "java" && @.years > 6)'::jsonpath
AND experience @? '$[*] ? (@.field == "ui/ux" && @.years > 2)'::jsonpath
LIMIT 3;
全自动化
How do I dynamically create a query without worrying about sql injection?
把上面的查询生成放到一个PL/pgSQL函数中动态执行:
CREATE OR REPLACE FUNCTION f_users_with_experience(_filter_arr jsonb, _limit int = 3)
RETURNS SETOF users
LANGUAGE plpgsql PARALLEL SAFE STABLE STRICT AS
$func$
DECLARE
_sql text;
BEGIN
-- assert (you may want to be stricter?)
IF jsonb_path_exists (_filter_arr, '$[*] ? (!exists(@.field) || !exists(@.years))') THEN
RAISE EXCEPTION 'Parameter (_filter_arr) must be a JSON array with keys "field" and "years" in every object. Invalid input was: >>%<<', _filter_arr;
END IF;
-- generate query string
SELECT INTO _sql
'SELECT * FROM users
WHERE experience @? '
|| string_agg(quote_nullable(format('$[*] ? (@.field == %s && @.years > %s)'
, f->'field'
, f->'years'))
, E'\nAND experience @? ')
|| E'\nLIMIT ' || _limit
FROM jsonb_array_elements(_filter_arr) f;
-- execute
IF _sql IS NULL THEN
RAISE EXCEPTION 'SQL statement is NULL. Should not occur!';
ELSE
-- RAISE NOTICE '%', _sql; -- debug first if in doubt
RETURN QUERY EXECUTE _sql;
END IF;
END
$func$;
致电:
SELECT * FROM f_users_with_experience('[{"field": "devops", "years": 5 },
, {"field": "backend dev", "years": 6}]');
或者用不同的 LIMIT
:
SELECT * FROM f_users_with_experience('[{"field": "devops", "years": 5 }]', 123);
db<>fiddle here
您应该熟悉 PL/pgSQL 才能使用并理解它。
SQL注入是不可能的因为...
- 有效JSON 输入被强制执行
- JSON 值与原始 JSON 双引号连接。
- 最重要的是,每个生成的
jsonpath
值都用quote_nullable()
单引号括起来。
在讨论 SQL/JSON 路径表达式时,我使用一个来断言有效输入:
jsonb_path_exists (_filter_arr, '$[*] ? (!exists(@.field) || !exists(@.years))')
检查 JSON 数组中的每个对象,以及是否缺少两个必需键(field
、years
)之一。