表名作为PostgreSQL函数参数


85

我想将表名作为Postgres函数中的参数传递。我尝试了这段代码:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

我得到了:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

这是我更改为以下错误select * from quote_ident($1) tab where tab.id=1

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

可能quote_ident($1)有效,因为如果没有where quote_ident($1).id=1得到的部分1,则表示已选定某项。为什么第一个quote_ident($1)工作和第二个工作不能同时进行?以及如何解决?


我知道这个问题有点陈旧,但是我在寻找另一个问题的答案时发现了它。您的函数不能只查询informational_schema吗?我的意思是,从某种意义上讲,它的用途是-让您查询并查看数据库中存在哪些对象。只是一个主意。
David S

@DavidS感谢您的评论,我会尝试的。
John Doe 2012年

Answers:


124

这可以进一步简化和改进:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

使用模式限定名称进行调用(请参见下文):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

要么:

SELECT some_f('"my very uncommon table name"');

要点

  • 使用OUT参数简化功能。您可以直接选择动态SQL的结果并完成。无需其他变量和代码。

  • EXISTS正是您想要的。您将获得true该行是否存在false。有多种方法可以做到这一点,EXISTS通常是最有效的。

  • 您似乎想要返回一个整数,因此我将boolean结果从转换EXISTSinteger,得到的正是您所拥有的。我会返回布尔值

  • 我将对象标识符类型regclass用作的输入类型_tbl。那会做所有quote_ident(_tbl)format('%I', _tbl)会做的事,但是会更好,因为:

  • ..它也可以防止SQL注入

  • ..如果表名无效/不存在/对当前用户不可见,它将立即失败,并且更正常地失败。(regclass参数仅适用于现有表。)

  • ..它可与模式限定的表名配合使用,在这些表名中,纯格式quote_ident(_tbl)format(%I)因无法解决歧义而失败。您将必须分别传递和转义模式名称和表名称。

  • 我仍然使用format(),因为它简化了语法(并演示了如何使用),但是使用%s代替%I。通常,查询更为复杂,因此format()可以提供更多帮助。对于简单的示例,我们还可以串联:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • 列表中id只有一个表时,无需对列进行表限定FROM。在此示例中,没有任何歧义。(动态)内部的SQL命令EXECUTE具有单独的作用域,函数变量或参数在那里不可见-与函数主体中的普通SQL命令相反。

这就是为什么您总是总是对动态SQL正确避开用户输入的原因:

db <> fiddle在这里演示SQL注入
Old sqlfiddle


2
@suhprano:好的。尝试一下:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter

为什么是%s而不是%L?
Lotus Lotus

3
@莲花:答案中有解释。regclass以文本形式输出时,值将自动转义。在这种情况下%L将是错误的
Erwin Brandstetter

CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; 创建表行计数功能,select table_rows('nf_part1');
l mingzhi

我们如何获得所有列?
Ashish

12

如果可能,请不要这样做。

这就是答案-这是一种反模式。如果客户端知道它要从中获取数据的表,则SELECT FROM ThatTable。如果以某种需要的方式设计数据库,则它似乎设计得不够理想。如果数据访问层需要知道表中是否存在值,则在该代码中编写SQL很容易,并且将该代码推送到数据库中也不好。

在我看来,这就像在电梯内安装设备,在其中可以键入所需楼层的数量。按下“开始”按钮后,它将机械手移至所需地板的正确按钮并按下。这引入了许多潜在的问题。

请注意:这里没有嘲弄的意图。我愚蠢的电梯示例是*我能想象的最好的设备*,用于简洁地指出此技术的问题。它添加了一个无用的间接层,使用晦涩难懂的服务器端SQL代码将表名的选择从调用者空间(使用健壮且易于理解的DSL,SQL)移动到混合形式。

通过将查询构造逻辑移动到动态SQL中来进行责任分解,使得代码更难以理解。它以充满潜在错误的自定义代码的名称违反了标准且可靠的约定(SQL查询如何选择要选择的内容)。

以下是有关此方法潜在问题的详细说明:

  • 动态SQL提供了难以在前端代码或后端代码中单独识别的SQL注入的可能性(必须一起检查它们才能看到此情况)。

  • 存储过程和函数可以访问SP /函数所有者有权访问的资源,但调用者没有权限。据我了解,无需特别注意,默认情况下,当您使用生成动态SQL的代码并运行它时,数据库将在调用者的权限下执行动态SQL。这意味着您要么根本无法使用特权对象,要么必须向所有客户端开放它们,从而增加了对特权数据进行潜在攻击的范围。在创建时将SP /功能设置为始终以特定用户身份运行(在SQL Server中EXECUTE AS)可能会解决该问题,但会使事情变得更加复杂。通过使动态SQL成为非常诱人的攻击媒介,这加剧了上一点中提到的SQL注入的风险。

  • 当开发人员必须了解应用程序代码在做什么才能对其进行修改或修复错误时,他会发现很难获得要执行的确切SQL查询。可以使用SQL事件探查器,但这具有特殊的特权,并且会对生产系统产生负面的性能影响。SP可以记录已执行的查询,但这会增加复杂性,从而带来可疑的好处(要求容纳新表,清除旧数据等),并且这种情况并不明显。实际上,某些应用程序的结构使得开发人员没有数据库凭据,因此对于他来说实际上看不到正在提交的查询几乎是不可能的。

  • 当发生错误时(例如,当您尝试选择一个不存在的表时),您将从数据库中收到一条消息,内容为“无效的对象名称”。无论您是在后端还是在数据库中编写SQL,都会发生完全相同的事情,但是不同之处在于,一些试图对系统进行故障排除的可怜的开发人员不得不在一个层次上钻一个更深的层次,而在另一个层次上,存在问题,以探究“全部解决”的奇迹过程来尝试找出问题所在。日志不会显示“ GetWidget中的错误”,而是显示“ OneProcedureToRuleThemAllRunner中的错误”。这种抽象通常会使系统变得更糟

基于参数切换表名的伪C#示例:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

尽管这并不能消除所有可能的问题,但此示例中没有我用其他技术概述的缺陷。


4
我并不完全同意。假设您按下此“执行”按钮,然后某些机制检查地板是否存在。函数可以在触发器中使用,而触发器又可以检查某些条件。这种想法可能并不是最漂亮的,但是如果系统已经足够大并且您需要对其逻辑进行一些校正,那么我认为这种选择并不那么引人注目。
约翰·多伊

1
但是请考虑,无论您如何处理,尝试按下不存在的按钮的操作只会产生异常。实际上,您不能按不存在的按钮,因此在按按钮的顶部添加检查不存在的数字的层没有任何好处,因为在创建该层之前该数字项就不存在了!我认为抽象是编程中最强大的工具。但是,添加仅很少复制现有抽象的层是错误的。数据库本身已经是将名称映射到数据集的抽象层。
ErikE

3
发现。SQL的重点是表达要提取的数据集。此功能唯一要做的就是封装“罐头” SQL语句。考虑到标识符也是经过硬编码的事实,整个东西都有难闻的气味。
Nick Hristov 2014年

1
@three除非有人处于技能精通阶段(请参阅Dreyfus技能获取模型),否则他应该完全遵守诸如“不要将表名传递到要在动态SQL中使用的过程中”之类的规则。甚至暗示它并不总是不好的,本身就是不好的建议。知道这一点,初学者将很想使用它!那很糟。只有某个主题的高手才能打破规则,因为他们是唯一有经验的人,在任何特定情况下都知道这种打破规则是否真正有意义。
ErikE 2015年

1
@ three-cups我确实更新了更多有关为什么这是一个坏主意的详细信息。
ErikE

10

在plpgsql代码内部,EXECUTE语句必须用于表名或列来自变量的查询。同样,IF EXISTS (<query>)query动态生成该构造时,该构造也是不允许的。

这是已修复两个问题的函数:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;

谢谢,几分钟前我在做同样的事情。唯一的区别是我必须删除quote_ident()它,因为它添加了额外的引号,这使我有些惊讶,因为在大多数示例中都使用了它。
约翰·多伊

如果/当表名包含[az]以外的字符时,或者如果/当它与保留标识符冲突(例如:“ group”作为表名)时,这些多余的引号将是必需的
DanielVérité2012年

并且,顺便说一句,您能否提供一个链接来证明该IF EXISTS <query>构造不存在?我很确定我将类似的内容视为工作代码示例。
约翰·多伊

1
@JohnDoe:IF EXISTS (<query>) THEN ...在plpgsql中是一个非常有效的构造。只是不使用动态SQL <query>。我经常使用。而且,此功能可以大大改善。我发布了答案。
Erwin Brandstetter,2012年

1
抱歉,您是正确if exists(<query>)的,在一般情况下它是有效的。刚刚检查并相应地修改了答案。
丹尼尔·韦里特(DanielVérité)2012年

4

第一个实际上并不像您所指的那样“起作用”,它仅在不产生错误的范围内起作用。

尝试一下SELECT * FROM quote_ident('table_that_does_not_exist');,您将看到为什么函数返回1的原因:select返回的表只有一列(名为quote_ident)和一行(变量$1或在这种情况下table_that_does_not_exist)。

您要执行的操作将需要动态SQL,实际上quote_*是要使用这些函数的地方。


非常感谢,马特,table_that_does_not_exist给出了相同的结果,您是对的。
约翰·多伊

2

如果问题是要测试表是否为空(id = 1),则这是Erwin存储过程的简化版本:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;

1

我知道这是一个旧线程,但是我最近在尝试解决相同问题时遇到了这个问题-就我而言,对于一些相当复杂的脚本。

将整个脚本转换为动态SQL是不理想的。这是繁琐且容易出错的工作,并且您失去了参数化的能力:必须将参数插值到SQL中的常量中,这会对性能和安全性造成不良后果。

这是一个简单的技巧,如果您只需要从表中进行选择,则可以使SQL保持完整-使用动态SQL创建临时视图:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;

0

如果希望将表名,列名和值动态传递给函数作为参数

使用此代码

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value

-2

我有9.4版本的PostgreSQL,并且我总是使用以下代码:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

然后:

SELECT add_new_table('my_table_name');

对我有用。

注意!上面的示例之一显示了“如果在查询数据库期间要保持安全性怎么办”:P


1
创建new表与使用现有表的名称进行操作不同。无论哪种方式,您都应转义作为代码执行的文本参数,否则您可以进行SQL注入。
Erwin Brandstetter,2015年

哦,是的,我的错。这个话题误导了我,此外我没有读到最后。通常以我为例。:P为什么带有文本参数的代码会被注入?
2015年

糟糕,这真的很危险。谢谢您的回答!
2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.