Рефакторинг функции PL/pgSQL для возврата результатов различных запросов SELECT

Я написал функцию, которая выводит запрос 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.

Определение базовых таблиц.

Переменная sensors будет содержать все столбцы, отображаемые здесь для соответствующей таблицы в type. Например: Если type - pcdmet, тогда 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. Я хочу автоматизировать это, запустить запрос автоматически и вернуть результат. Как я могу это сделать?

Ответ 1

Динамический SQL и RETURN тип

(я сохранил лучшее для последнего, продолжаю читать!)
Вы хотите выполнить динамический SQL. В принципе, это просто в plpgsql с помощью EXECUTE. Вам не нужен курсор - на самом деле, большую часть времени вам лучше без явных курсоров.
Найдите примеры SO с поиском.

Проблема, с которой вы столкнулись: вы хотите вернуть записи еще undefined типа. Функция должна объявлять возвращаемый тип с помощью RETURNS (или с помощью OUT или INOUT параметры). В вашем случае вам придется вернуться к анонимным записям, потому что число, имена и типы возвращаемых столбцов различаются. Как:

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

Но как бы вы это сделали, когда заранее не знаете столбцы?
Вы можете использовать менее структурированные типы данных документа, такие как json, jsonb, hstore или xml:

Но для целей этого вопроса предположим, что вы хотите как можно больше возвращать индивидуальные, правильно напечатанные и именованные столбцы.

Простое решение с фиксированным обратным типом

Столбец 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 менее запутанным. $1 в SQL-строке не ссылается на параметр функции, а на значение, переданное с предложением 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)

Как мы должны определить тип 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;


Различные полные типы таблиц

Если вы действительно пытаетесь вернуть все столбцы таблицы (например, одну из таблиц на связанной странице, затем используйте это простое, очень мощное решение с полиморфным типом:

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 автоматически определяет тип строки (составной тип данных) для каждой созданной таблицы, поэтому для каждой таблицы есть четко определенный тип. Это включает временные таблицы, которые удобны для ad-hoc-использования.

  • Любой тип может быть NULL. Таким образом, мы передаем значение NULL, отбрасываем тип таблицы.

  • Теперь функция возвращает четко определенный тип строки, и мы можем использовать SELECT * FROM data_of(...) для разложения строки и получения отдельных столбцов.

  • pg_typeof(_tbl_type) возвращает имя таблицы как тип идентификатора объекта regtype. При автоматическом преобразовании в text идентификаторы автоматически дублируются и имеют сертификат, если необходимо. Поэтому SQL-инъекция не является возможной. Это может даже иметь дело с табличными именами где quote_ident() не работает.

Ответ 2

Вероятно, вы захотите вернуть 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;

Ответ 3

Извините, но ваш вопрос очень неясен. Однако ниже вы найдете самостоятельный пример того, как создавать и использовать функцию, которая возвращает переменную курсора. Надеюсь, это поможет!

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;