表名作为 PostgreSQL 函数参数

Posted

技术标签:

【中文标题】表名作为 PostgreSQL 函数参数【英文标题】:Table name as a PostgreSQL function parameter 【发布时间】:2012-05-29 03:44:09 【问题描述】:

我想在 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 吗?我的意思是,这在某种程度上就是为了让您查询并查看数据库中存在哪些对象。只是一个想法。 @DavidS 感谢您的评论,我会尝试的。 【参考方案1】:

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

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 通常是最有效的。

你似乎想要一个 integer 返回,所以我将 boolean 结果从 EXISTS 转换为 integer,这正是你所拥有的。我会返回boolean

我使用对象标识符类型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'

FROM 列表中只有一个表时,无需对id 列进行表限定。在这个例子中不可能有歧义。 (动态)EXECUTE 内的 SQL 命令有一个单独的范围,函数变量或参数在那里不可见 - 与函数体中的普通 SQL 命令相反。

这就是为什么您总是正确地为动态 SQL 转义用户输入的原因:

dbfiddle here 演示 SQL 注入旧 sqlfiddle

【讨论】:

@suhprano:当然。试试看:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$; 为什么是 %s 而不是 %L? @Lotus:解释在答案中。 regclass 值在作为文本输出时会自动转义。在这种情况下,%L错误 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'); 如何获取所有列?【参考方案2】:

如果可能的话,不要这样做。

这就是答案——它是一种反模式。如果客户端知道它想要从中获取数据的表,那么SELECT FROM ThatTable。如果数据库的设计方式是需要的,那么它的设计似乎不是最理想的。如果数据访问层需要知道某个值是否存在于表中,那么在那个代码中编写 SQL 是很容易的,并且把这个代码推入数据库是不好的。

在我看来,这就像在电梯内安装一个设备,人们可以在其中输入所需楼层的编号。按下 Go 按钮后,它将机械手移至所需楼层的正确按钮并按下它。这会带来许多潜在的问题。

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

这种通过将查询构造逻辑移动到动态 SQL 中的职责拆分使得代码更难理解。它以可能出错的自定义代码的名义违反了标准且可靠的约定(SQL 查询如何选择要选择的内容)。

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

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

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

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

当发生错误时,例如当您尝试选择一个不存在的表时,您会从数据库中收到一条类似于“无效对象名称”的消息。无论您是在后端还是在数据库中编写 SQL,这都会发生完全相同的情况,但不同的是,一些试图对系统进行故障排除的可怜的开发人员必须深入到另一个洞穴下面的另一个洞穴中。问题存在,深入挖掘“Does It All”的奇妙过程,试图找出问题所在。日志不会显示“GetWidget 中的错误”,它会显示“OneProcedureToRuleThemAllRunner 中的错误”。这种抽象通常会使系统更糟

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

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

虽然这并不能消除所有可以想象的问题,但我在其他技术中概述的缺陷在此示例中不存在。

【讨论】:

我不完全同意这一点。比如说,你按下这个“开始”按钮,然后一些机制会检查地板是否存在。函数可以用在触发器中,而触发器又可以检查某些条件。这个决定可能不是最漂亮的,但如果系统已经足够大,你需要对其逻辑进行一些更正,那么,我想这个选择并不那么戏剧化。 但请考虑尝试按下不存在的按钮的操作,无论您如何处理它都会简单地产生异常。您实际上不能按下不存在的按钮,因此在按下按钮的基础上添加一个层来检查不存在的数字并没有任何好处,因为在您创建所述层之前,这样的数字条目并不存在!在我看来,抽象是编程中最强大的工具。但是,添加一个仅能很好地复制现有抽象的层是错误。数据库本身已经是一个将名称映射到数据集的抽象层。 正确。 SQL 的全部意义在于表达您想要提取的数据集。这个函数唯一要做的就是封装一个“固定的”SQL 语句。鉴于标识符也是硬编码的,所以整个东西都有一股难闻的气味。 @three 在某人处于掌握阶段(参见the Dreyfus model of skill acquisition)技能之前,他应该绝对遵守诸如“不要将表名传递到过程中”之类的规则用于动态 SQL”。甚至暗示它并不总是坏事本身就是坏建议。知道这一点,初学者会很想使用它!那很糟。只有掌握某个主题的大师才能打破规则,因为只有他们有经验才能知道在任何特定情况下这种打破规则是否真的有意义。 @three-cups 我确实更新了更多关于为什么这是一个坏主意的细节。【参考方案3】:

在 plpgsql 代码中,EXECUTE 语句必须用于表名或列来自变量的查询。当动态生成query 时,也不允许使用IF EXISTS (<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] 之外的字符,或者如果/当它与保留标识符冲突时(例如:“组”作为表名),则需要这些额外的引号跨度> 顺便问一下,您能否提供一个链接来证明IF EXISTS <query> 构造不存在?我很确定我看到了类似的东西作为工作代码示例。 @JohnDoe: IF EXISTS (<query>) THEN ... 是 plpgsql 中一个完全有效的构造。只是不适用于<query> 的动态 SQL。我经常使用它。另外,这个功能还可以改进不少。我发布了一个答案。 对不起,if exists(<query>) 是对的,它在一般情况下是有效的。刚刚检查并相应地修改了答案。【参考方案4】:

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

将整个脚本变成动态 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;

【讨论】:

它现在甚至是一个较旧的线程:)。以防万一,“临时”要求架构也是临时的。您可以省略该关键字并根据需要进行清理。除了这里的正统讨论之外,它至少对于某些管理任务来说是一种有用的技术。【参考方案5】:

按照您的意思,第一个实际上并不“有效”,它仅在不产生错误的情况下有效。

试试SELECT * FROM quote_ident('table_that_does_not_exist');,你会明白为什么你的函数返回1:选择返回一个表,其中有一列(名为quote_ident)和一行(变量$1或在这种特殊情况下为table_that_does_not_exist )。

您想要做的将需要动态 SQL,这实际上是使用 quote_* 函数的地方。

【讨论】:

非常感谢,Matt,table_that_does_not_exist 给出了相同的结果,你是对的。【参考方案6】:

如果问题是测试表是否为空(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;

【讨论】:

【参考方案7】:

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

使用此代码

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

【讨论】:

【参考方案8】:

我有 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

【讨论】:

创建new 表不同于使用现有表的名称进行操作。无论哪种方式,您都应该转义作为代码执行的文本参数,否则您可以接受 SQL 注入。 哦,是的,我的错误。这个话题误导了我,另外我没有读到最后。通常在我的情况下。 :P 为什么带有文本参数的代码会被注入? 糟糕,这真的很危险。谢谢你的回答!

以上是关于表名作为 PostgreSQL 函数参数的主要内容,如果未能解决你的问题,请参考以下文章

将表名作为参数传递时 PL/SQL 函数不起作用

在 MySQL 中:如何将表名作为存储过程和/或函数参数传递?

将表名作为输入参数动态传递并使用它[重复]

存储过程,将表名作为参数传递

在 UDF 中声明表变量以输入表名作为参数

函数名作为参数传递与回调函数