web-dev-qa-db-fra.com

Renvoyer plusieurs champs sous forme d'enregistrement dans PostgreSQL avec PL / pgSQL

J'écris un SP en utilisant PL/pgSQL.
Je souhaite renvoyer un enregistrement comprenant des champs de plusieurs tables différentes. Pourrait ressembler à quelque chose comme ça:

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; 

Comment puis-je retourner les champs de différentes tables sous forme de champs dans un seul enregistrement?

[Edit]

J'ai réalisé que l'exemple que j'ai donné ci-dessus était un peu trop simpliste. Certains des champs que je dois extraire seront enregistrés en tant que lignes distinctes dans la table de la base de données interrogée, mais je souhaite les renvoyer dans la structure d'enregistrement "aplatie".

Le code ci-dessous devrait aider à illustrer davantage:

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
63
skyeagle

Vous devez définir un nouveau type et définir votre fonction pour renvoyer ce type.

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; 

Si vous souhaitez renvoyer plusieurs enregistrements, vous devez définir la fonction comme suit: returns setof my_type


Mise à jour

Une autre option consiste à utiliser RETURNS TABLE() au lieu de créer un TYPE introduit dans Postgres 8.4.

CREATE OR REPLACE FUNCTION get_object_fields(name text) 
  RETURNS TABLE (f1 varchar(10), f2 varchar(10) /* , ... */ )
...
56

N'utilisez pas CREATE TYPE pour renvoyer un résultat polymorphe. Utilisez plutôt le type RECORD et en abusez. Vérifiez-le:

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;

Faites attention au fait qu'il peut éventuellement renvoyer deux ou trois colonnes en fonction de l'entrée.

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

test=> SELECT test_ret('barbaz','foo');
             test_ret             
----------------------------------
 (f,foobarbaz)
(1 row)

Cela provoque des ravages dans le code, utilisez donc un nombre cohérent de colonnes, mais il est ridiculement pratique pour renvoyer des messages d'erreur facultatifs avec le premier paramètre renvoyant le succès de l'opération. Réécrit en utilisant un nombre constant de colonnes:

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;

Presque au chaud épique:

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)

Mais comment divisez-vous cela en plusieurs lignes afin que votre couche de choix ORM puisse convertir les valeurs en types de données natifs de la langue de votre choix? La chaleur:

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)

C’est l’une des fonctionnalités les plus cool et les plus sous-utilisées de PostgreSQL. S'il vous plaît répandre la Parole.

104
Sean

Cela peut être plus simple avec OUT paramètres :

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;
  • Vous n'avez pas besoin pour créer un type uniquement dans l'intérêt de cette fonction plpgsql. Cela peut peut être utile si vous souhaitez lier deux fonctions au même type. Je ne l'utilise plus que rarement depuis que OUT paramètres ont été ajoutés.

  • Comme vous l'avez peut-être remarqué, il n'y a pas d'instruction RETURN. Les paramètres OUT sont renvoyés automatiquement. Aucune instruction RETURN n'est nécessaire.

  • Étant donné que les paramètres OUT sont visibles partout dans le corps de la fonction (et peuvent être utilisés comme toute autre variable), assurez-vous de qualifier les colonnes du même nom pour éviter les conflits de noms.

Encore plus simple - ou renvoyer plusieurs lignes

La plupart du temps, cela peut être simplifié davantage. Parfois, les requêtes dans le corps de la fonction peuvent être combinées, ce qui est généralement (pas toujours) plus rapide. Et vous pouvez utiliser RETURNS TABLE() - introduit avec Postgres 8.4 (bien avant que cette question ne soit posée également ).

L'exemple ci-dessus pourrait être réécrit comme suit:

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 Revient effectivement à avoir un groupe de OUT paramètres combinés avec RETURNS record, Juste un peu plus court/plus élégant.

  • La principale différence est que cette fonction peut renvoyer 0, 1 ou plusieurs lignes, tandis que la première version toujours renvoie 1 ligne.
    Si vous voulez vous en assurer, celui-ci ne renvoie que 0 ou 1 ligne, ajoutez LIMIT 1 Comme indiqué.

  • RETURN QUERY Est le moyen très pratique et moderne de renvoyer directement les résultats d'une requête.
    Vous pouvez utiliser plusieurs instances dans une même fonction pour ajouter plus de lignes à la sortie.

Différents types de rangées

Si votre fonction est supposée renvoyer de manière dynamique des résultats avec différents types de lignes , en fonction de l'entrée, lisez la suite:

49
Erwin Brandstetter

Si vous avez une table avec cette disposition d'enregistrement exacte, utilisez son nom comme type, sinon vous devrez déclarer le type explicitement:

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; 
5
Quassnoi

Vous pouvez y parvenir en utilisant simplement comme ensemble de retours d'enregistrements en utilisant une requête de retour.

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$

Et appelez cette fonction comme: select * from schemaName.get_two_users_from_school(schoolid) as x(a bigint, b varchar);

2
Ritesh Jha

vous pouvez le faire en utilisant le paramètre OUT et 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;

puis utilisez-le comme une table:

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

ou

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

ou

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