重构 PL/pgSQL 函数以返回各种 SELECT 查询的输出

Posted

技术标签:

【中文标题】重构 PL/pgSQL 函数以返回各种 SELECT 查询的输出【英文标题】:Refactor a PL/pgSQL function to return the output of various SELECT queries 【发布时间】:2012-07-29 05:28:33 【问题描述】:

我编写了一个函数,它以文本形式输出格式良好的 PostgreSQL SELECT 查询。现在我不想再输出文本了,但实际上对数据库运行生成的 SELECT 语句并返回结果 - 就像查询本身一样。

到目前为止我所拥有的:

CREATE OR REPLACE FUNCTION data_of(integer)
  RETURNS text AS
$BODY$
DECLARE
   sensors varchar(100);   -- holds list of column names
   type    varchar(100);   -- holds name of table
   result  text;           -- holds SQL query
       -- declare more variables

BEGIN
      -- do some crazy stuff

      result := 'SELECT\r\nDatahora,' || sensors ||
      '\r\n\r\nFROM\r\n' || type ||
      '\r\n\r\nWHERE\r\id=' || $1 ||'\r\n\r\nORDER BY Datahora;';

      RETURN result;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION data_of(integer) OWNER TO postgres;

sensors 保存表type 的列名列表。这些是在函数的过程中声明和填充的。最终,它们的值如下:

sensors:'column1, column2, column3' 除了Datahora (timestamp),所有列的类型都是double precision

type :'myTable' 可以是四个表之一的名称。每个都有不同的列,除了公共列Datahora

Definition of the underlying tables.

变量sensors 将保存type 中相应表的所有 列。例如:如果typepcdmet,那么sensors 将是'datahora,dirvento,precipitacao,pressaoatm,radsolacum,tempar,umidrel,velvento'

变量用于构建存储在result 中的SELECT 语句。喜欢:

SELECT Datahora, column1, column2, column3
FROM   myTable
WHERE  id=20
ORDER  BY Datahora;

现在,我的函数将此语句返回为text。我复制粘贴并在 pgAdmin 中或通过 psql 执行它。我想自动化这个,自动运行查询并返回结果。我该怎么做?

【问题讨论】:

我冒昧地重写了你的问题以澄清事情 - 现在我想我大部分都理解了。如果您不同意,请随时回滚。 同意 :) 感谢您的改进!我喜欢这个社区的运作方式 【参考方案1】:

您可能希望返回cursor。试试这样的(我没试过):

CREATE OR REPLACE FUNCTION data_of(integer)
  RETURNS refcursor AS
$BODY$
DECLARE
      --Declaring variables
      ref refcursor;
BEGIN
      -- make sure `sensors`, `type`, $1 variable has valid value
      OPEN ref FOR 'SELECT Datahora,' || sensors ||
      ' FROM ' || type ||
      ' WHERE nomepcd=' || $1 ||' ORDER BY Datahora;';
      RETURN ref;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION data_of(integer) OWNER TO postgres;

【讨论】:

它不起作用。它返回一个名为 refcursor 的列,以及一个带有“”的行!我不是 PL/PgSQL 的专家,但我并不傻。这是我的第一次尝试。顺便说一句,感谢您的尝试! :) 您是否从返回的游标中提取?喜欢 FETCH ref INTO target; Cunsult 游标文档以获取有关如何使用游标的详细信息。 postgresql.org/docs/8.1/static/plpgsql-cursors.html 惊人的,非常优雅和强大的方法。适用于 PgAdmin,但不适用于 PSQLODBC 驱动程序和 ADODB。在 PgAdmin 中,我们必须在 中添加显式 fetch all 以使其正常工作。 如何使用它? FETCH data_of(1)?我不断收到语法错误【参考方案2】:

很抱歉,您的问题非常不清楚。但是,您将在下面找到一个自包含示例,如何创建和使用返回游标变量的函数。希望对您有所帮助!

begin;

create table test (id serial, data1 text, data2 text);

insert into test(data1, data2) values('one', 'un');
insert into test(data1, data2) values('two', 'deux');
insert into test(data1, data2) values('three', 'trois');

create function generate_query(query_name refcursor, columns text[])
returns refcursor 
as $$
begin
  open query_name for execute 
    'select id, ' || array_to_string(columns, ',') || ' from test order by id';
  return query_name;
end;
$$ language plpgsql;

select generate_query('english', array['data1']);
fetch all in english;

select generate_query('french', array['data2']);
fetch all in french;
move absolute 0 from french; -- do it again !
fetch all in french;

select generate_query('all_langs', array['data1','data2']);
fetch all in all_langs;

-- this will raise in runtime as there is no data3 column in the test table
select generate_query('broken', array['data3']);

rollback;

【讨论】:

不要说这是一个不清楚的问题,而是告诉我缺少什么,或者你想知道什么。我很抱歉没有说清楚,尽管我很努力地提出了一个好问题。如果您考虑,我可以改进它。 @waldyr.ar:你更新的版本现在好多了。只是不清楚您想要实现什么,并且缺少一些细节(那些 sensorstype 是什么以及它们如何获得它们的值)。如果问题包含说明问题的最简单的可能工作代码(即可以由其他人执行的代码),通常会有很大帮助。【参考方案3】:

动态 SQL 和 RETURN 类型

(我把最好的留到最后,继续阅读!) 您要执行动态 SQL。原则上,在EXECUTE 的帮助下,这在plpgsql 中很简单。您不需要 光标。事实上,大多数情况下,没有显式游标会更好。

您遇到的问题:您想要返回尚未定义类型的记录。函数需要在RETURNS 子句中声明其返回类型(或使用OUTINOUT 参数)。在您的情况下,您将不得不回退到匿名记录,因为返回的列的 numbernamestypes 各不相同。喜欢:

CREATE FUNCTION data_of(integer)
  RETURNS SETOF record AS ...

但是,这并不是特别有用。您必须在每次调用时提供列定义列表。喜欢:

SELECT * FROM data_of(17)
AS foo (colum_name1 integer
      , colum_name2 text
      , colum_name3 real);

但是,如果您事先不知道列,您会怎么做呢? 您可以使用结构较少的文档数据类型,例如 jsonjsonbhstorexml。见:

How to store a data table in database?

但是,就这个问题而言,我们假设您希望尽可能多地返回单独、正确键入和命名的列。

具有固定返回类型的简单解决方案

datahora 列似乎是给定的,我假设数据类型为timestamp,并且总是有另外两列具有不同的名称和数据类型。

名称我们将放弃返回类型中的通用名称。类型我们也将放弃,并将所有内容转换为 text因为每个数据类型都可以转换为text

CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, col2 text, col3 text)
  LANGUAGE plpgsql AS
$func$
DECLARE
   _sensors text := 'col1::text, col2::text';  -- cast each col to text
   _type    text := 'foo';
BEGIN
   RETURN QUERY EXECUTE '
      SELECT datahora, ' || _sensors || '
      FROM   ' || quote_ident(_type) || '
      WHERE  id = $1
      ORDER  BY datahora'
   USING  _id;
END
$func$;

变量_sensors_type可以改为输入参数。

注意RETURNS TABLE 子句。

注意RETURN QUERY EXECUTE 的使用。这是从动态查询中返回行的更优雅的方法之一。

我为函数参数命名,只是为了让RETURN QUERY EXECUTEUSING 子句不那么混乱。 SQL 字符串中的$1 不是指函数参数,而是指使用USING 子句传递的值。 (在这个简单的示例中,两者恰好在各自的范围内都是 $1。)

注意_sensors 的示例值:每一列都转换为text 类型。

这种代码很容易受到SQL injection 的攻击。我使用quote_ident() 来防范它。将变量_sensors 中的几个列名集中在一起可以防止使用quote_ident()(这通常是个坏主意!)。确保没有坏东西可以通过其他方式出现,例如通过 quote_ident() 单独运行列名。想到一个VARIADIC 参数...

自 PostgreSQL 9.1 起更简单

对于 9.1 或更高版本,您可以使用format() 进一步简化:

RETURN QUERY EXECUTE format('
   SELECT datahora, %s  -- identifier passed as unescaped string
   FROM   %I            -- assuming the name is provided by user
   WHERE  id = $1
   ORDER  BY datahora'
  ,_sensors, _type)
USING  _id;

同样,单个列名可以正确转义,这将是一种干净的方式。

不同数量的列共享相同类型

在您的问题更新后,您的返回类型似乎有

可变的列数 但所有列的类型double precision(别名float8

在这种情况下使用ARRAY 类型来嵌套可变数量的值。此外,我返回一个列名数组:

CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, names text[], values float8[])
  LANGUAGE plpgsql AS
$func$
DECLARE
   _sensors text := 'col1, col2, col3';  -- plain list of column names
   _type    text := 'foo';
BEGIN
   RETURN QUERY EXECUTE format('
      SELECT datahora
           , string_to_array($1)  -- AS names
           , ARRAY[%s]            -- AS values
      FROM   %s
      WHERE  id = $2
      ORDER  BY datahora'
    , _sensors, _type)
   USING  _sensors, _id;
END
$func$;

各种完整的表格类型

要真正返回表格的所有列,有一个使用polymorphic type 的简单而强大的解决方案:

CREATE OR REPLACE FUNCTION data_of(_tbl_type anyelement, _id int)
  RETURNS SETOF anyelement
  LANGUAGE plpgsql AS
$func$
BEGIN
   RETURN QUERY EXECUTE format('
      SELECT *
      FROM   %s  -- pg_typeof returns regtype, quoted automatically
      WHERE  id = $1
      ORDER  BY datahora'
    , pg_typeof(_tbl_type))
   USING  _id;
END
$func$;

打电话(重要!):

SELECT * FROM data_of(NULL::pcdmet, 17);

将调用中的pcdmet 替换为任何其他表名。

这是如何工作的?

anyelement 是伪数据类型,多态类型,任何非数组数据类型的占位符。函数中所有出现的anyelement 都会评估为运行时提供的相同类型。通过将定义类型的值作为参数提供给函数,我们隐式定义了返回类型。

PostgreSQL 自动为每个创建的表定义一个行类型(一种复合数据类型),因此每个表都有一个定义良好的类型。这包括临时表,方便临时使用。

任何类型都可以是NULL。提交NULL 值,转换为表格类型:NULL::pcdmet

现在该函数返回一个定义明确的行类型,我们可以使用SELECT * FROM data_of() 来分解行并获取各个列。

pg_typeof(_tbl_type) 将表的名称返回为object identifier type regtype。当自动转换为text 时,如果需要,标识符会自动双引号和模式限定,从而自动防御 SQL 注入。这甚至可以处理quote_ident() 会失败的模式限定表名。见:

Table name as a PostgreSQL function parameter

【讨论】:

非常感谢!所有这些帮助都是无价的!并确保它解决了问题! 很好的答案,但不禁觉得前两个解决方案只是最后一个非常酷的解决方案的前戏;-) 最终解决方案是否可以包装在一个以关系的文本名称作为参数的函数中?我遇到过几次这个问题,我有像getData(String relationName, Int rowId)这样的功能的Web应用程序,我想通过调用类似上面的方法来实现,但是relationName存在SQL注入的危险。 @BrianPreslopsky: pg_typeof(_tbl_type)::text @Jabinator1:您可以从 plpgsql 函数中的字符串生成表名,但是您无法通过这种方式获取动态返回类型,因为这是由传递 in 类型。 IOW:SQL 强制您最迟在调用时声明返回类型。【参考方案4】:
# copy paste me into bash shell directly
clear; IFS='' read -r -d '' sql_code << 'EOF_SQL_CODE'
CREATE OR REPLACE FUNCTION func_get_all_users_roles()
  -- define the return type of the result set as table
  -- those datatypes must match the ones in the src
  RETURNS TABLE (
                 id           bigint
               , email        varchar(200)
               , password     varchar(200)
               , roles        varchar(100)) AS
$func$
BEGIN
   RETURN QUERY 
   -- start the select clause
   SELECT users.id, users.email, users.password, roles.name as roles
   FROM user_roles
   LEFT JOIN roles ON (roles.guid = user_roles.roles_guid)
   LEFT JOIN users ON (users.guid = user_roles.users_guid)
   -- stop the select clause
;
END
$func$  LANGUAGE plpgsql;
EOF_SQL_CODE
# create the function
psql -d db_name -c "$sql_code"; 

# call the function 
psql -d db_name -c "select * from func_get_all_users_roles() "

【讨论】:

问错了吗?

以上是关于重构 PL/pgSQL 函数以返回各种 SELECT 查询的输出的主要内容,如果未能解决你的问题,请参考以下文章

如何在 pl/pgsql 中创建返回 refcursor 和 totalRow 的函数/过程?

如何在 PL/pgSQL 中按行类型返回表

PL/pgSQL 函数在 pgAdmin 之外无法正确运行

从 PL/PGSQL 引用会话变量 (\set var='value')

在 PL/pgSQL 函数中使用变量

简单的 PL/pgSQL 函数的错误