PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Refactoriser une fonction PL/pgSQL pour renvoyer la sortie de diverses requêtes SELECT

SQL dynamique et RETURN taper


Vous souhaitez exécuter du SQL dynamique . En principe, c'est simple en plpgsql avec l'aide de EXECUTE . Vous n'avez pas besoin un curseur. En fait, la plupart du temps, il vaut mieux se passer de curseurs explicites.

Le problème que vous rencontrez :vous voulez retourner des enregistrements de type encore indéfini . Une fonction doit déclarer son type de retour dans le RETURNS clause (ou avec OUT ou INOUT paramètres). Dans votre cas, vous auriez à recourir à des enregistrements anonymes, car numéro , noms et types des colonnes renvoyées varient. Comme :

CREATE FUNCTION data_of(integer)
  RETURNS SETOF record AS ...

Cependant, ce n'est pas particulièrement utile. Vous devez fournir une liste de définition de colonne avec chaque appel. Comme :

SELECT * FROM data_of(17)
AS foo (colum_name1 integer
      , colum_name2 text
      , colum_name3 real);

Mais comment feriez-vous cela même si vous ne connaissez pas les colonnes à l'avance ?
Vous pouvez utiliser des types de données de document moins structurés comme json , jsonb , hstore ou xml . Voir :

  • Comment stocker une table de données dans la base ?

Mais, pour les besoins de cette question, supposons que vous souhaitiez autant que possible renvoyer des colonnes individuelles, correctement typées et nommées.

Solution simple avec type de retour fixe

La colonne datahora semble être une donnée, je suppose que le type de données timestamp et qu'il y a toujours deux autres colonnes avec des noms et des types de données différents.

Noms nous abandonnerons au profit des noms génériques dans le type de retour.
Types nous allons aussi abandonner et tout convertir en text depuis chaque le type de données peut être converti en 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$;

Les variables _sensors et _type pourraient être des paramètres d'entrée à la place.

Notez le RETURNS TABLE clause.

Notez l'utilisation de RETURN QUERY EXECUTE . C'est l'une des façons les plus élégantes de renvoyer des lignes à partir d'une requête dynamique.

J'utilise un nom pour le paramètre de fonction, juste pour faire le USING clause de RETURN QUERY EXECUTE moins déroutant. $1 dans la chaîne SQL ne fait pas référence au paramètre de la fonction mais à la valeur passée avec le USING clause. (Il se trouve que les deux coûtent $1 dans leur portée respective dans cet exemple simple.)

Notez l'exemple de valeur pour _sensors  :chaque colonne est convertie en type text .

Ce type de code est très vulnérable à l'injection SQL . J'utilise quote_ident() pour s'en protéger. Regrouper quelques noms de colonnes dans la variable _sensors empêche l'utilisation de quote_ident() (et c'est généralement une mauvaise idée !). Assurez-vous qu'aucune mauvaise chose ne peut s'y trouver d'une autre manière, par exemple en exécutant individuellement les noms de colonne via quote_ident() Au lieu. Un VARIADIC paramètre me vient à l'esprit...

Plus simple depuis PostgreSQL 9.1

Avec la version 9.1 ou ultérieure, vous pouvez utiliser format() pour simplifier encore :

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;

Encore une fois, les noms de colonne individuels pourraient être échappés correctement et seraient la méthode la plus simple.

Nombre variable de colonnes partageant le même type

Après la mise à jour de votre question, il semble que votre type de retour a

  • un nombre variable de colonnes
  • mais toutes les colonnes du même type double precision (alias float8 )

Utiliser un ARRAY type dans ce cas pour imbriquer un nombre variable de valeurs. De plus, je renvoie un tableau avec des noms de colonnes :

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

Divers types de tableaux complets

Pour renvoyer réellement toutes les colonnes d'une table , il existe une solution simple et puissante utilisant un type polymorphe :

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

Appel (important !) :

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

Remplacer pcdmet dans l'appel avec n'importe quel autre nom de table.

Comment ça marche ?

anyelement est un pseudo-type de données, un type polymorphe, un espace réservé pour tout type de données non-tableau. Toutes les occurrences de anyelement dans la fonction évaluer au même type fourni au moment de l'exécution. En fournissant une valeur d'un type défini comme argument à la fonction, nous définissons implicitement le type de retour.

PostgreSQL définit automatiquement un type de ligne (un type de données composite) pour chaque table créée, il existe donc un type bien défini pour chaque table. Cela inclut les tables temporaires, ce qui est pratique pour une utilisation ad hoc.

Tout type peut être NULL . Remettez un NULL valeur, convertie en type de table :NULL::pcdmet .

Maintenant, la fonction renvoie un type de ligne bien défini et nous pouvons utiliser SELECT * FROM data_of() pour décomposer la ligne et obtenir des colonnes individuelles.

pg_typeof(_tbl_type) renvoie le nom de la table comme type d'identifiant d'objet regtype . Lorsqu'il est automatiquement converti en text , les identifiants sont automatiquement entre guillemets doubles et qualifiés par le schéma si nécessaire, défense contre l'injection SQL automatiquement. Cela peut même traiter des noms de table qualifiés de schéma où quote_ident() échouerait. Voir :

  • Nom de table en tant que paramètre de fonction PostgreSQL