Postgres jsonb 查询动态值

Posted

技术标签:

【中文标题】Postgres jsonb 查询动态值【英文标题】:Postgres jsonb query for dynamic values 【发布时间】:2021-12-12 02:46:02 【问题描述】:

在 users 表中,我有一个 jsob 列 experience 具有以下 json 结构:

[
    
        "field": "devops",
        "years": 9
    ,
    
        "field": "backend dev",
        "years": 7
     
... // could be N number of objects with different values
]

业务需求

客户可以要求在任何领域都有经验并且在每个领域都有各自多年经验的人

这是一个示例查询

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注入?

技术栈

Nodejs 打字稿 类型ORM Postgres

【问题讨论】:

【参考方案1】:

这是一个参数化查询,因此或多或少是注入安全的。 qualifies 标量子查询计算 experience 是否满足所有请求项。参数为$1(请求参数的jsonb数组)和$2(限制值)。您可能需要根据环境的风格更改它们的语法。

select t.* from 
(
  select u.*,
    (
      select count(*) = jsonb_array_length($1)
      from jsonb_array_elements(u.experience) ej -- jsonb list of experiences 
      inner join jsonb_array_elements($1) 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 $2;

一些解释

qualifies 子查询的逻辑是这样的:首先“规范化”experience 并将 jsonb 数组请求到“表”中,然后在目标条件下将它们内部连接(在本例中为 field_a = field_b and years_a >= years_b)和计算其中有多少匹配。如果计数等于请求项的数量(即count(*) = jsonb_array_length($1)),那么所有请求项都得到满足,因此experience 符合条件。 因此不需要动态 SQL。我认为这种方法也可以重复使用。

【讨论】:

【参考方案2】:

索引

首先,您需要索引支持。我建议使用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()。见:

Find rows containing a key in a JSONB array of records

动态生成查询

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;

全自动化

如何动态创建查询而不用担心sql注入?

把上面的查询生成放到一个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 $2 (_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小提琴here

您应该熟悉 PL/pgSQL 才能使用并理解它。

SQL 注入 是不可能的,因为 ...

    强制执行有效的 JSON 输入 JSON 值与原始 JSON 双引号连接。 最重要的是,每个生成的jsonpath 值都用quote_nullable() 单引号引起来。

在讨论 SQL/JSON 路径表达式时,我使用一个来断言有效输入:

jsonb_path_exists (_filter_arr, '$[*] ? (!exists(@.field) || !exists(@.years))')

检查 JSON 数组中的每个对象以及是否缺少两个必需键(fieldyears)之一。

【讨论】:

非常感谢您抽出时间详细解释一切。我不知道您在答案中涵盖的许多概念。研究了他们并学到了很多东西。在我的项目中也使用它。再次感谢!!

以上是关于Postgres jsonb 查询动态值的主要内容,如果未能解决你的问题,请参考以下文章

Json 类型列元素更新 - Postgres

Postgres:查询多个jsonb字段

结合关系查询提高 Postgres jsonb 查询的性能

变量替换查询postgres jsonb对象

如何在 postgres 中对 jsonb 值求和?

Postgres检查jsonb数组是不是不包含值返回空结果集