99

我想在 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)工作和第二个不能同时工作?这怎么能解决?

4

8 回答 8

148

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

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结果从EXISTSto 转换为integer,这正是您所拥有的。我会返回布尔值

我使用对象标识符类型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 here演示 SQL 注入
Old sqlfiddle

于 2012-05-22T23:07:21.583 回答
13

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

这就是答案——它是一种反模式。如果客户端知道它想要从中获取数据的表,那么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);

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

于 2012-07-11T00:20:02.803 回答
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;
于 2012-05-22T18:49:52.880 回答
6

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

将整个脚本变成动态 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;
于 2020-05-18T18:47:07.483 回答
4

从您的意思来看,第一个实际上并没有“工作”,它仅在不产生错误的情况下才有效。

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

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

于 2012-05-22T16:35:02.570 回答
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;
于 2017-12-21T14:24:16.450 回答
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
于 2018-08-28T10:35:08.993 回答
-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

于 2015-04-30T12:15:13.093 回答