表名作为PostgreSQL函数参数

Joh*_*Doe 69 postgresql function dynamic-sql plpgsql identifier

我想在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');
Run Code Online (Sandbox Code Playgroud)

我得到了这个:

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 "."
Run Code Online (Sandbox Code Playgroud)

以下是更改为此时出现的错误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...
Run Code Online (Sandbox Code Playgroud)

可能,quote_ident($1)有效,因为没有where quote_ident($1).id=1我得到的部分1,这意味着选择了一些东西.为什么第一个quote_ident($1)工作和第二个工作不能同时工作?这怎么可以解决?

Erw*_*ter 104

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

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

使用模式限定名称调用(见下文):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()
Run Code Online (Sandbox Code Playgroud)

要么:

SELECT some_f('"my very uncommon table name"');
Run Code Online (Sandbox Code Playgroud)

主要观点

  • 使用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'
    
    Run Code Online (Sandbox Code Playgroud)
  • idFROM列表中只有一个表时,无需对列进行表限定.在这个例子中没有歧义.(动态)SQL命令里面EXECUTE有一个单独的作用域,函数变量或参数在那里是不可见的 - 而不是函数体中的普通SQL命令.

使用PostgreSQL 9.1进行测试.format()至少需要该版本.

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

SQL Fiddle演示SQL注入

  • @suhprano:好的.试一试:`DO $$ BEGIN EXECUTE'ALYYZE mytbl'; END $$;` (2认同)
  • @Lotus:解释就在答案中.输出为文本时,`regclass`值会自动转义.在这种情况下,'%L`将*错误*. (2认同)

Eri*_*ikE 12

不要这样做.

这就是答案.这是一种可怕的反模式.它有什么用途?如果客户端知道它想要数据的表,那么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,情况都会完全相同,但不同的是,一些正在尝试对系统进行故障排除的可怜的开发人员必须将一个级别深入到另一个洞穴之下的另一个洞穴中.问题确实存在,深入了解"尽力而为"的奇迹程序,并试图找出问题所在.日志不会显示"GetWidget中的错误",它将显示"OneProcedureToRuleThemAllRunner中的错误".这种抽象只会让你的系统变得更糟.

这是基于参数的伪C#切换表名称中更好的示例:

string sql = string.Format("SELECT * FROM {0};", EscapeSqlIdentifier(tableName));
results = connection.Execute(sql);
Run Code Online (Sandbox Code Playgroud)

我在这个例子中完全没有提到我用其他技术提到的每个缺陷.

向存储过程提交表名没有任何目的,没有好处,没有可能的改进.

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

Dan*_*ité 9

在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;
Run Code Online (Sandbox Code Playgroud)


小智 9

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

将整个脚本变成动态 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;
Run Code Online (Sandbox Code Playgroud)