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

时间:2012-07-31 12:38:51

标签: sql database postgresql plpgsql dynamic-sql

我写了一个函数,输出一个以文本形式形成的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'
    Datahoratimestamp)外,所有列均为double precision类型。

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

Definition of the underlying tables.

变量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中执行它。我想自动执行此操作,自动运行查询并返回结果。我怎么能这样做?

4 个答案:

答案 0 :(得分:69)

动态SQL和RETURN类型

(我最后保存了最好的,继续阅读!)
您想要执行动态SQL 。原则上,在EXECUTE的帮助下,这在plpgsql中很简单。你不需要一个光标 - 事实上,大多数情况下你没有明确的游标会更好。 Find examples on SO with a search

您遇到的问题:您希望返回尚未定义类型的记录。函数需要使用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);

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

但是出于这个问题的目的,让我们假设你想尽可能多地返回单独的,正确输入和命名的列。

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

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

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

CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, col2 text, col3 text) 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$ LANGUAGE plpgsql;

这是如何工作的?

  • 变量_sensors_type可能是输入参数。

  • 请注意RETURNS TABLE子句。

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

  • 我为函数参数使用了一个名称,只是为了使USING RETURN QUERY EXECUTE子句更容易混淆。 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

由于我们必须定义函数的RETURN类型,在这种情况下我采用ARRAY类型,它可以包含可变数量的值。另外,我返回一个包含列名的数组,因此您也可以解析结果中的名称:

CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, names text[], values float8[] ) 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$  LANGUAGE plpgsql;


各种完整的表格类型

如果您实际上尝试返回表格的所有列(例如tables at the linked page之一),请使用这个简单,非常强大的解决方案polymorphic type

CREATE OR REPLACE FUNCTION data_of(_tbl_type anyelement, _id int)
  RETURNS SETOF anyelement 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$ LANGUAGE plpgsql;

呼叫:

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

将呼叫中的pcdmet替换为任何其他表名。

这是如何工作的?

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

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

  • 任何类型都可以是NULL。所以我们提交NULL值,转换为表格类型。

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

  • pg_typeof(_tbl_type)将表格的名称作为object identifier type regtype返回。自动转换为text后,如果需要,标识符会自动双引号和架构限定。因此,SQL注入是不可能的。这甚至可以处理模式限定的表名where quote_ident() would fail

答案 1 :(得分:3)

您可能想要返回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;

答案 2 :(得分: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;

答案 3 :(得分: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() "