sql - 动态值的 Postgres jsonb 查询

标签 sql node.js postgresql sequelize.js typeorm

在用户表中,我有一个 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 注入(inject)?

技术栈

  • Node
  • typescript
  • 类型ORM
  • Postgres

最佳答案

索引

首先,您需要索引支持。我建议使用 jsonb_path_ops 索引,例如:

CREATE INDEX users_experience_gin_idx ON users USING gin (experience jsonb_path_ops);

参见:

查询

还有一个可以利用该索引的查询(100% 等同于您的原始索引):

SELECT *
FROM   users
WHERE  experience @? '$[*] ? (@.field == "devops" && @.years > 5 )'
AND    experience @? '$[*] ? (@.field == "backend dev" && @.years > 5)'
LIMIT  3;

需要 Postgres 12 或更高版本,其中添加了 SQL/JSON 路径语言。

索引支持绑定(bind)到 Postgres 中的运算符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 $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<> fiddle here

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

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

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

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

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

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

关于sql - 动态值的 Postgres jsonb 查询,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69727991/

相关文章:

sql - PostgreSQL:仅当元素唯一时才将元素附加到 jsonb 数组

postgresql - 如何创建条件和选择?

MySQL Pivot 将记录转为列出所有数据的列

java - 有没有办法从拆分函数中获得一致的结果?(相同数量的单词)

node.js - 是否可以使用子文件夹将 sails 中的 Controller 分组?

node.js - 如何将串口传入的缓冲区数据转换为nodejs

javascript - 重定向时将数据传递到我的 ejs View

mysql - 使用 PostgreSQL 模式文件将 mySQL 数据移动到 postgres

如果其中一列已更改,则 PostgreSQL 触发器

MySQL 左连接限制为一行