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

42

我编写了一个函数,输出一个文本形式的 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(时间戳)外,所有列都是double precision类型。

  • type'myTable'
    可以是四个表之一的名称。它们都有不同的列,除了通用列Datahora

底层数据表的定义.

变量sensors将为与type中对应表格的所有显示列保存。例如:如果typepcdmet,则sensors将为'datahora,dirvento,precipitacao,pressaoatm,radsolacum,tempar,umidrel,velvento'

这些变量被用来构建一个SELECT语句存储在result中。例如:

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

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


我冒昧修改了你的问题以澄清事情 - 现在我认为我大部分理解了。如果您不同意,可以随时回滚。 - Erwin Brandstetter
同意 :) 谢谢您的改进!我喜欢这个社区的运作方式。 - waldyr.ar
4个回答

104

动态SQL和RETURN类型

(继续阅读,我为您留了最好的内容!)
您想执行动态SQL。在plpgsql中,借助EXECUTE,这很简单。您不需要一个游标。事实上,在大多数情况下,没有显式的游标更好。

您遇到的问题是:您想返回未定义类型的记录。函数需要在RETURNS子句(或使用OUTINOUT参数)中声明其返回类型。在您的情况下,您必须退回到匿名记录,因为返回列的数字名称类型是不确定的。例如:

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);

但是如果你不知道列名,该怎么办呢?
你可以使用 less structured document 数据类型,例如 json, jsonb, hstorexml。请参见:如何在数据库中存储数据表?

但是,对于这个问题的目的,让我们假设您想尽可能返回单独,正确命名和类型的列。

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

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注入攻击。 我使用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$;

各种完整的表格类型

要实际返回一个表的所有列,有一种简单而强大的解决方案,即使用多态类型

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)对象标识符类型regtype的形式返回表的名称。当自动转换为text时,如果需要,标识符会自动加上双引号和架构限定符,自动防御SQL注入。这甚至可以处理带有模式限定表名的情况,而quote_ident()会失败。请参见:


1
非常感谢!所有的帮助都是无价之宝!而且肯定解决了问题! - waldyr.ar
1
很棒的答案,但是我感觉前两个解决方案只是为了最后一个非常酷的解决方案做铺垫。;-) - beldaz
@beldaz:请为这个问题开一个新的提问。评论不是合适的地方。你可以随时添加一个链接到这个答案来提供背景信息。 - Erwin Brandstetter
1
@BrianPreslopsky: pg_typeof(_tbl_type)::text @BrianPreslopsky:pg_typeof(_tbl_type)::text - Erwin Brandstetter
1
@Jabinator1:你可以在plpgsql函数内部从字符串生成表名,但是你无法以这种方式获取动态返回类型,因为这是通过传递类型来确定的。换句话说:SQL强制你在调用时最晚声明返回类型。 - Erwin Brandstetter
显示剩余14条评论

4

您可能需要返回一个游标。尝试像这样做(我还没有试过):

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的列和一行带有“<未命名门户XX>”的内容!我不是PL/PgSQL的大师,但我也不傻。这是我的第一次尝试。顺便说一句,谢谢你的尝试! :) - waldyr.ar
你从返回的游标中FETCH了吗?例如:FETCH ref INTO target; 请参考游标文档以获取有关如何使用游标的详细信息。http://www.postgresql.org/docs/8.1/static/plpgsql-cursors.html - bpgergo
非常优雅和强大的方法。在PgAdmin中运行良好,但与PSQLODBC驱动程序和ADODB不兼容。在PgAdmin中,我们必须添加显式的“fetch all in <cursor_name>”才能使其正常工作。 - Anatoly Alekseev
怎么使用它?是 FETCH data_of(1) 吗?我一直在那里收到语法错误。 - Vinicius Dantas

1
很抱歉,但是您的问题非常不清楚。不过,在下面,您将找到一个自包含的示例,说明如何创建和使用返回游标变量的函数。希望它能有所帮助!
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;

1
不要说这是一个不清楚的问题,告诉我缺了什么,或者你想知道什么。很抱歉我的问题没有表达清楚,尽管我努力做了一个好问题。我可以根据你的建议进行改进。 - waldyr.ar
@waldyr.ar:你更新的版本现在好多了。之前不太清楚你想要实现什么,而且有些细节也没有提供(那些“传感器”和“类型”是什么,它们如何获取值)。通常情况下,如果一个问题包含最简单可行的工作代码(即可以被其他人执行的代码),以说明问题,这会帮助很多。 - user272735

0
# 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() "

错误的问题? - cstork

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接