Возврат нескольких полей в виде записи в PostgreSQL с PL / pgSQL - PullRequest
62 голосов
/ 28 декабря 2010

Я пишу SP, используя PL / pgSQL.
Я хочу вернуть запись, состоящую из полей из нескольких разных таблиц. Может выглядеть примерно так:

CREATE OR REPLACE FUNCTION get_object_fields(name text)
  RETURNS RECORD AS $$
BEGIN
  -- fetch fields f1, f2 and f3 from table t1
  -- fetch fields f4, f5 from table t2
  -- fetch fields f6, f7 and f8 from table t3
  -- return fields f1 ... f8 as a record
END
$$ language plpgsql; 

Как я могу вернуть поля из разных таблиц как поля в одной записи?

[Изменить]

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

Код ниже должен помочь проиллюстрировать далее:

CREATE TABLE user (id int, school_id int, name varchar(32));

CREATE TYPE my_type (
  user1_id   int,
  user1_name varchar(32),
  user2_id   int,
  user2_name varchar(32)
);

CREATE OR REPLACE FUNCTION get_two_users_from_school(schoolid int)
  RETURNS my_type AS $$
DECLARE
  result my_type;
  temp_result user;
BEGIN
  -- for purpose of this question assume 2 rows returned
  SELECT id, name INTO temp_result FROM user where school_id = schoolid LIMIT 2;
  -- Will the (pseudo)code below work?:
  result.user1_id := temp_result[0].id ;
  result.user1_name := temp_result[0].name ;
  result.user2_id := temp_result[1].id ;
  result.user2_name := temp_result[1].name ;
  return result ;
END
$$ language plpgsql

Ответы [ 6 ]

103 голосов
/ 22 мая 2011

Не используйте CREATE TYPE , чтобы вернуть полиморфный результат.Используйте вместо этого RECORD .Проверьте это:

CREATE FUNCTION test_ret(a TEXT, b TEXT) RETURNS RECORD AS $$
DECLARE 
  ret RECORD;
BEGIN
  -- Arbitrary expression to change the first parameter
  IF LENGTH(a) < LENGTH(b) THEN
      SELECT TRUE, a || b, 'a shorter than b' INTO ret;
  ELSE
      SELECT FALSE, b || a INTO ret;
  END IF;
RETURN ret;
END;$$ LANGUAGE plpgsql;

Обратите внимание на тот факт, что при желании он может возвращать два или три столбца в зависимости от ввода.

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

CREATE FUNCTION test_ret(a TEXT, b TEXT) RETURNS RECORD AS $$
DECLARE 
  ret RECORD;
BEGIN
  -- Note the CASTING being done for the 2nd and 3rd elements of the RECORD
  IF LENGTH(a) < LENGTH(b) THEN
      ret := (TRUE, (a || b)::TEXT, 'a shorter than b'::TEXT);
  ELSE
      ret := (FALSE, (b || a)::TEXT, NULL::TEXT);
   END IF;
RETURN ret;
END;$$ LANGUAGE plpgsql;

Почти до эпической жары:

test=> SELECT test_ret('foobar','bar');
   test_ret    
----------------
 (f,barfoobar,)
(1 row)

test=> SELECT test_ret('foo','barbaz');
             test_ret             
----------------------------------
 (t,foobarbaz,"a shorter than b")
(1 row)

Но как разделить это на несколько строк, чтобы ваш слой ORM мог выбратьпреобразовать значения в родные типы данных вашего языка?Жаркость:

test=> SELECT a, b, c FROM test_ret('foo','barbaz') AS (a BOOL, b TEXT, c TEXT);
 a |     b     |        c         
---+-----------+------------------
 t | foobarbaz | a shorter than b
(1 row)

test=> SELECT a, b, c FROM test_ret('foobar','bar') AS (a BOOL, b TEXT, c TEXT);
 a |     b     | c 
---+-----------+---
 f | barfoobar | 
(1 row)

Это одна из самых крутых и недостаточно используемых функций в PostgreSQL.Пожалуйста, распространите слово.

55 голосов
/ 28 декабря 2010

Вам нужно определить новый тип и определить вашу функцию для возврата этого типа.

CREATE TYPE my_type AS (f1 varchar(10), f2 varchar(10) /* , ... */ );

CREATE OR REPLACE FUNCTION get_object_fields(name text) 
RETURNS my_type 
AS 
$$

DECLARE
  result_record my_type;

BEGIN
  SELECT f1, f2, f3
  INTO result_record.f1, result_record.f2, result_record.f3
  FROM table1
  WHERE pk_col = 42;

  SELECT f3 
  INTO result_record.f3
  FROM table2
  WHERE pk_col = 24;

  RETURN result_record;

END
$$ LANGUAGE plpgsql; 

Если вы хотите вернуть более одной записи, вам нужно определить функцию как returns setof my_type


Обновление

Другой вариант - использовать RETURNS TABLE() вместо создания TYPE, который был представлен в Postgres 8.4

CREATE OR REPLACE FUNCTION get_object_fields(name text) 
  RETURNS TABLE (f1 varchar(10), f2 varchar(10) /* , ... */ )
...
49 голосов
/ 27 марта 2013

Это может быть проще с OUT параметрами :

CREATE OR REPLACE FUNCTION get_object_fields(
          name text
    ,OUT user1_id   int
    ,OUT user1_name varchar(32)
    ,OUT user2_id   int
    ,OUT user2_name varchar(32)
) AS 
$func$
BEGIN
    SELECT t.user1_id, t.user1_name
    INTO     user1_id,   user1_name
    FROM   tbl1 t
    WHERE  t.tbl1_id = 42;

    user2_id := user1_id + 43; -- some calculation

    SELECT t.user2_name
    INTO     user2_name
    FROM   tbl2 t
    WHERE  t.tbl2_i = user2_id;
END
$func$ LANGUAGE plpgsql;
  • Вам не нужно для создания типа только ради этой функции plpgsql. Это может быть полезным, если вы хотите связать пару функций с одним и тем же типом. Я редко использую его больше, так как были добавлены OUT параметры.

  • Как вы могли заметить, оператор RETURN не существует. OUT параметры возвращаются автоматически, оператор RETURN не требуется.

  • Поскольку параметры OUT видны повсюду в теле функции (и могут использоваться точно так же, как и любая другая переменная), убедитесь, что столбцы таблицы с одинаковыми именами определены, чтобы избежать конфликтов имен.

Еще проще - или вернуть несколько строк

В большинстве случаев это можно упростить еще больше. Иногда запросы в теле функции могут быть объединены, что обычно (не всегда) быстрее. И вы можете использовать RETURNS TABLE() - введено в Postgres 8.4 (задолго до того, как был задан этот вопрос).

Пример сверху можно переписать так:

CREATE OR REPLACE FUNCTION get_object_fields(name text)
  RETURNS TABLE (
     user1_id   int
    ,user1_name varchar(32)
    ,user2_id   int
    ,user2_name varchar(32)) AS 
$func$
BEGIN
    RETURN QUERY
    SELECT t1.user1_id, t1.user1_name, t2.user2_id, t2.user2_name
    FROM   tbl1 t1
    JOIN   tbl2 t2 ON t2.user2_id = t1.user1_id + 43
    WHERE  t1.tbl1_id = 42
    LIMIT  1;  -- may be optional
END
$func$ LANGUAGE plpgsql; 
  • RETURNS TABLE - фактически то же самое, что набор OUT параметров в сочетании с RETURNS record, только немного короче / элегантнее.

  • Основное отличие состоит в том, что эта функция может возвращать 0, 1 или много строк, тогда как первая версия всегда возвращает 1 строку.
    Если вы хотите убедиться, что этот возвращает только 0 или 1 строку, добавьте LIMIT 1, как показано.

  • RETURN QUERY - очень удобный современный способ возврата результатов запроса напрямую.
    Вы можете использовать несколько экземпляров в одной функции, чтобы добавить больше строк к выводу.

Различные типы строк

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

5 голосов
/ 28 декабря 2010

Если у вас есть таблица с таким точным макетом записи, используйте ее имя в качестве типа, в противном случае вам придется объявить тип явно:

CREATE OR REPLACE FUNCTION get_object_fields
        (
        name text
        )
RETURNS mytable
AS
$$
        DECLARE f1 INT;
        DECLARE f2 INT;
        …
        DECLARE f8 INT;
        DECLARE retval mytable;
        BEGIN
        -- fetch fields f1, f2 and f3 from table t1
        -- fetch fields f4, f5 from table t2
        -- fetch fields f6, f7 and f8 from table t3
                retval := (f1, f2, …, f8);
                RETURN retval;
        END
$$ language plpgsql; 
2 голосов
/ 12 апреля 2018

Этого можно добиться, просто используя набор возвращаемых записей, используя запрос возврата.

CREATE OR REPLACE FUNCTION schemaName.get_two_users_from_school(schoolid bigint)
 RETURNS SETOF record
 LANGUAGE plpgsql
AS $function$
begin

 return query
  SELECT id, name FROM schemaName.user where school_id = schoolid;

end;
$function$

И вызвать эту функцию как: select * from schemaName.get_two_users_from_school(schoolid) as x(a bigint, b varchar);

0 голосов
/ 16 декабря 2014

Вы можете сделать это, используя параметр OUT и CROSS JOIN

CREATE OR REPLACE FUNCTION get_object_fields(my_name text, OUT f1 text, OUT f2 text)
AS $$
SELECT t1.name, t2.name
FROM  table1 t1 
CROSS JOIN table2 t2 
WHERE t1.name = my_name AND t2.name = my_name;
$$ LANGUAGE SQL;

, а затем использовать его в качестве таблицы:

select get_object_fields( 'Pending') ;
get_object_fields
-------------------
(Pending,code)
(1 row)

или

select * from get_object_fields( 'Pending');
f1    |   f
---------+---------
Pending | code
(1 row)

или

select (get_object_fields( 'Pending')).f1;
f1
---------
Pending
(1 row)
...