捕获受动态 sql 影响的行数?
Capture number of rows affected by dynamic sql?
我正在尝试从 plpgsql 函数中的 QUERY EXEUTE
获取 return,以便能够检查有多少行受到动态更新查询的影响。我的用例是在插入或更新到动态集 table 时将事件(带有自定义负载)添加到单独的 table。因为我的事件有自定义负载,所以我无法使用数据库触发器(例如插入前触发)。作为一个简化的例子,假设我有这个 table:
CREATE TABLE users (user_id text primary key, name text)
这是我简化的事件table:
CREATE TABLE events(event_id text primary key, payload json)
这是我的简化函数:
CREATE OR REPLACE FUNCTION my_function(_rowtype anyelement, q text, payload jsonb)
RETURNS SETOF anyelement AS
$func$
DECLARE
event_id text;
BEGIN
SELECT jsonb_object_field_text (payload, 'id')::text INTO STRICT event_id;
execute format('insert into event(event_id, payload) values (, )') using event_id, payload;
RETURN QUERY EXECUTE format('%s', q);
END
$func$ LANGUAGE plpgsql;
我们的目标是让这项工作完全相同,就像有人在交易中创建这些一样。在插入的伪代码中:
BEGIN
insert into events(id, payload) values(, )
insert into users(columns) values(<any values>)
COMMIT
更新也类似:
BEGIN
insert into events(id, payload) values(, )
result, error := query(`update users set name = 'hello' where id = 'Not Exists Thus No Rows Modified'`);
if result.rowsAffected() == 0 {
ROLLBACK
}
COMMIT
函数 my_function
除了一种极端情况外几乎可以正常工作:当更新实际上不影响任何行时。
例如,这有效:
select * from my_function(NULL::users,
'insert into users(id,name) values('u1', ''a2'') returning *',
payload => '{"id": "e1", "custom": "s1", "field": "2019-10-12T07:20:50.52Z"}')
正如预期的那样,在完成此操作后,用户 table 和事件 table 中的一行都已创建。
失败的是以下内容:
select * from my_function(NULL::users,
'update users set name = ''hello'' where user_id = ''NotExists'' returning *',
payload => '{"id": "e2", "custom": "s3", "field": "2019-10-12T07:20:50.52Z"}')
这里,在事件中创建了一行table(我的目标是不应该创建)。
我知道这种方法并不优雅,而且我知道这很容易受到 SQL 注入的攻击。我喜欢关于解决此问题的更好方法的建议(包括取消我们现在正在做的事情)。但是为了直接回答这个问题,我希望存储 QUERY EXECUTE
的结果,检查是否有任何行受到影响,并引发错误,这样就不会出现事件 [=39= 中的行的情况]是在用户没有真正对应的变化时创建的table。 Users table 只是一个例子,一般来说,它可以是任何动态设置 table.
A RETURN QUERY 不需要走到函数的末尾,它只说:“这个查询的结果是结果集的一部分”。
因此您可以使用 RETURN QUERY,请求 FOUND 并采取相应行动。这是为以这种方式工作而修改的函数:
CREATE OR REPLACE FUNCTION public.my_function(_rowtype anyelement, q text, payload jsonb)
RETURNS SETOF anyelement
LANGUAGE plpgsql
AS $function$
DECLARE
event_id text;
BEGIN
SELECT jsonb_object_field_text (payload, 'id')::text INTO STRICT event_id;
RETURN QUERY EXECUTE format('%s', q);
IF FOUND THEN
execute format('insert into events(event_id, payload) values (, )') using event_id, payload;
END IF;
RETURN;
END
$function$
PD:也许您还可以使用转换表 OLD 和 NEW(从 v10 开始可用,https://www.postgresql.org/docs/10/sql-createtrigger.html)通过触发器 FOR EACH STATEMENT 解决您的问题
CREATE OR REPLACE FUNCTION my_function(_rowtype anyelement, q text, payload jsonb)
RETURNS SETOF anyelement
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE q;
IF NOT FOUND THEN
RETURN; -- nothing happened yet, we can exit silently.
-- Or you WANT an error for this case. Then do this instead:
-- RAISE EXCEPTION 'Query passed in parameter "q" did not affect any rows. Doing nothing!';
END IF;
INSERT INTO event(event_id, payload)
VALUES (payload->>'id', payload);
END
$func$;
正如所评论的那样,RETURN QUERY
不会从函数中 return。 The manual:
RETURN NEXT
and RETURN QUERY
do not actually return from the
function — they simply append zero or more rows to the function's
result set. Execution then continues with the next statement in the
PL/pgSQL function. As successive RETURN NEXT
or RETURN QUERY
commands are executed, the result set is built up. A final RETURN
,
which should have no argument, causes control to exit the function (or
you can just let control reach the end of the function).
手册中该章的底部正好有一个针对您的情况的 code example。实际上,从我这里。起源于此:
- FUNCTION syntax error
建议使用 GET DIAGNOSTICS
而不是更简单的 FOUND
。 EXECUTE
确实没有设置 FOUND
的状态。但是 RETURN QUERY
确实如此。所以继续使用更简单的 FOUND
。相关:
- Dynamic SQL (EXECUTE) as condition for IF statement
您的原件中有 format()
两次。虽然这通常对动态 SQL 非常有用,但在您的情况下却毫无用处。 EXECUTE format('%s', q)
与 EXECUTE q
完全相同,只是增加了成本。两者都是在传递用户输入时为 SQL 注入打开的大门。
虽然事务很有可能被回滚,但从关键步骤开始,其余的稍后再做。避免浪费工作。所以我将 executing q
移动到顶部。 假设它不依赖于现在稍后插入的“有效负载”行。
另外,INSERT INTO events
可以是普通的SQL。那里没有动态。不需要 format()
或 EXECUTE
.
最后,假设您的 jsonb_object_field_text (payload, 'id')::text
只是 payload->>'id'
的另一种说法。不需要额外的变量和另一个 SELECT INTO
.
针对 SQL 注入的警告
将用户输入(示例中的参数 q
)转换为动态执行的代码是所有注入漏洞中最直接的 SQL。我可不想那样做时被夹在内衣里。
我正在尝试从 plpgsql 函数中的 QUERY EXEUTE
获取 return,以便能够检查有多少行受到动态更新查询的影响。我的用例是在插入或更新到动态集 table 时将事件(带有自定义负载)添加到单独的 table。因为我的事件有自定义负载,所以我无法使用数据库触发器(例如插入前触发)。作为一个简化的例子,假设我有这个 table:
CREATE TABLE users (user_id text primary key, name text)
这是我简化的事件table:
CREATE TABLE events(event_id text primary key, payload json)
这是我的简化函数:
CREATE OR REPLACE FUNCTION my_function(_rowtype anyelement, q text, payload jsonb)
RETURNS SETOF anyelement AS
$func$
DECLARE
event_id text;
BEGIN
SELECT jsonb_object_field_text (payload, 'id')::text INTO STRICT event_id;
execute format('insert into event(event_id, payload) values (, )') using event_id, payload;
RETURN QUERY EXECUTE format('%s', q);
END
$func$ LANGUAGE plpgsql;
我们的目标是让这项工作完全相同,就像有人在交易中创建这些一样。在插入的伪代码中:
BEGIN
insert into events(id, payload) values(, )
insert into users(columns) values(<any values>)
COMMIT
更新也类似:
BEGIN
insert into events(id, payload) values(, )
result, error := query(`update users set name = 'hello' where id = 'Not Exists Thus No Rows Modified'`);
if result.rowsAffected() == 0 {
ROLLBACK
}
COMMIT
函数 my_function
除了一种极端情况外几乎可以正常工作:当更新实际上不影响任何行时。
例如,这有效:
select * from my_function(NULL::users,
'insert into users(id,name) values('u1', ''a2'') returning *',
payload => '{"id": "e1", "custom": "s1", "field": "2019-10-12T07:20:50.52Z"}')
正如预期的那样,在完成此操作后,用户 table 和事件 table 中的一行都已创建。 失败的是以下内容:
select * from my_function(NULL::users,
'update users set name = ''hello'' where user_id = ''NotExists'' returning *',
payload => '{"id": "e2", "custom": "s3", "field": "2019-10-12T07:20:50.52Z"}')
这里,在事件中创建了一行table(我的目标是不应该创建)。
我知道这种方法并不优雅,而且我知道这很容易受到 SQL 注入的攻击。我喜欢关于解决此问题的更好方法的建议(包括取消我们现在正在做的事情)。但是为了直接回答这个问题,我希望存储 QUERY EXECUTE
的结果,检查是否有任何行受到影响,并引发错误,这样就不会出现事件 [=39= 中的行的情况]是在用户没有真正对应的变化时创建的table。 Users table 只是一个例子,一般来说,它可以是任何动态设置 table.
A RETURN QUERY 不需要走到函数的末尾,它只说:“这个查询的结果是结果集的一部分”。
因此您可以使用 RETURN QUERY,请求 FOUND 并采取相应行动。这是为以这种方式工作而修改的函数:
CREATE OR REPLACE FUNCTION public.my_function(_rowtype anyelement, q text, payload jsonb)
RETURNS SETOF anyelement
LANGUAGE plpgsql
AS $function$
DECLARE
event_id text;
BEGIN
SELECT jsonb_object_field_text (payload, 'id')::text INTO STRICT event_id;
RETURN QUERY EXECUTE format('%s', q);
IF FOUND THEN
execute format('insert into events(event_id, payload) values (, )') using event_id, payload;
END IF;
RETURN;
END
$function$
PD:也许您还可以使用转换表 OLD 和 NEW(从 v10 开始可用,https://www.postgresql.org/docs/10/sql-createtrigger.html)通过触发器 FOR EACH STATEMENT 解决您的问题
CREATE OR REPLACE FUNCTION my_function(_rowtype anyelement, q text, payload jsonb)
RETURNS SETOF anyelement
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE q;
IF NOT FOUND THEN
RETURN; -- nothing happened yet, we can exit silently.
-- Or you WANT an error for this case. Then do this instead:
-- RAISE EXCEPTION 'Query passed in parameter "q" did not affect any rows. Doing nothing!';
END IF;
INSERT INTO event(event_id, payload)
VALUES (payload->>'id', payload);
END
$func$;
正如所评论的那样,RETURN QUERY
不会从函数中 return。 The manual:
RETURN NEXT
andRETURN QUERY
do not actually return from the function — they simply append zero or more rows to the function's result set. Execution then continues with the next statement in the PL/pgSQL function. As successiveRETURN NEXT
orRETURN QUERY
commands are executed, the result set is built up. A finalRETURN
, which should have no argument, causes control to exit the function (or you can just let control reach the end of the function).
手册中该章的底部正好有一个针对您的情况的 code example。实际上,从我这里。起源于此:
- FUNCTION syntax error
建议使用 GET DIAGNOSTICS
而不是更简单的 FOUND
。 EXECUTE
确实没有设置 FOUND
的状态。但是 RETURN QUERY
确实如此。所以继续使用更简单的 FOUND
。相关:
- Dynamic SQL (EXECUTE) as condition for IF statement
您的原件中有 format()
两次。虽然这通常对动态 SQL 非常有用,但在您的情况下却毫无用处。 EXECUTE format('%s', q)
与 EXECUTE q
完全相同,只是增加了成本。两者都是在传递用户输入时为 SQL 注入打开的大门。
虽然事务很有可能被回滚,但从关键步骤开始,其余的稍后再做。避免浪费工作。所以我将 executing q
移动到顶部。 假设它不依赖于现在稍后插入的“有效负载”行。
另外,INSERT INTO events
可以是普通的SQL。那里没有动态。不需要 format()
或 EXECUTE
.
最后,假设您的 jsonb_object_field_text (payload, 'id')::text
只是 payload->>'id'
的另一种说法。不需要额外的变量和另一个 SELECT INTO
.
针对 SQL 注入的警告
将用户输入(示例中的参数 q
)转换为动态执行的代码是所有注入漏洞中最直接的 SQL。我可不想那样做时被夹在内衣里。