web-dev-qa-db-fra.com

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

J'ai écrit une fonction qui génère une requête PostgreSQL SELECT bien formée sous forme de texte. Maintenant, je ne veux plus afficher de texte, mais en fait exécuter l'instruction SELECT générée sur la base de données et retourner le résultat - tout comme la requête elle-même le ferait.

Ce que j'ai jusqu'à présent:

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 contient la liste des noms de colonnes pour la table type. Ceux-ci sont déclarés et remplis au cours de la fonction. Finalement, ils détiennent des valeurs comme:

  • sensors: 'column1, column2, column3'
    À l'exception de Datahora (timestamp) toutes les colonnes sont de type double precision.

  • type: 'myTable'
    Peut être le nom d'une des quatre tables. Chacune a des colonnes différentes, à l'exception de la colonne commune Datahora.

Définition des tables sous-jacentes .

La variable sensors contiendra tous colonnes affichées ici pour le tableau correspondant dans type. Par exemple: si type est pcdmet alors sensors sera 'datahora,dirvento,precipitacao,pressaoatm,radsolacum,tempar,umidrel,velvento'

Les variables sont utilisées pour créer une instruction SELECT qui est stockée dans result. Comme:

SELECT Datahora, column1, column2, column3
FROM   myTable
WHERE  id=20
ORDER  BY Datahora;

À l'heure actuelle, ma fonction renvoie cette instruction sous la forme text. Je copie-colle et l'exécute dans pgAdmin ou via psql. Je veux automatiser cela, exécuter la requête automatiquement et retourner le résultat. Comment puis je faire ça?

33
waldyr.ar

SQL dynamique et type RETURN

(J'ai gardé le meilleur pour la fin, continuez à lire!)
Vous souhaitez exécuter SQL dynamique . En principe, c'est simple dans plpgsql avec l'aide de EXECUTE . Vous n'avez pas besoin d'un curseur - en fait, la plupart du temps, vous êtes mieux sans curseurs explicites.
Trouvez des exemples sur SO avec une recherche .

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

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

Cependant, ce n'est pas particulièrement utile. De cette façon, vous devrez fournir une liste de définition de colonne à chaque appel de la fonction. Comme:

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

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

Mais dans le cadre de cette question, supposons que vous souhaitiez renvoyer autant que possible des colonnes individuelles correctement tapé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 un nom et un type de données variables.

Noms nous abandonnerons en faveur des noms génériques dans le type de retour.
Types nous abandonnerons aussi, et jetterons tout dans text puisque toutes les données le type peut être converti en 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;

Comment cela marche-t-il?

  • Les variables _sensors Et _type Pourraient plutôt être des paramètres d'entrée.

  • Notez la clause RETURNS TABLE .

  • Notez l'utilisation de RETURN QUERY EXECUTE . C'est l'un des moyens les plus élégants de renvoyer des lignes à partir d'une requête dynamique.

  • J'utilise un nom pour le paramètre de fonction, juste pour rendre la clause USING de RETURN QUERY EXECUTE Moins confuse. $1 Dans la chaîne SQL ne fait pas référence au paramètre de fonction mais à la valeur passée avec la clause USING. (Les deux se trouvent être $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 à injection SQL . J'utilise quote_ident() pour me protéger contre cela. Le regroupement de 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 information incorrecte ne puisse s'y trouver d'une autre manière, par exemple en exécutant individuellement les noms de colonne via quote_ident() Un paramètre VARIADIC vient à l'esprit ...

Plus simple avec PostgreSQL 9.1+

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

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 colonnes individuels pourraient être échappés correctement et seraient la bonne façon.

Nombre variable de colonnes partageant le même type

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

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

Comme nous devons définir le type RETURN d'une fonction, j'ai recours à un type ARRAY dans ce cas, qui peut contenir un nombre variable de valeurs. De plus, je retourne un tableau avec des noms de colonnes, afin que vous puissiez également analyser les noms du résultat:

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;


Différents types de tables complètes

Si vous essayez de renvoyer toutes les colonnes d'une table (par exemple l'une des tables de la page liée , alors utilisez cette solution simple et très puissante avec un type polymorphe :

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;

Appel:

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

Remplacez pcdmet dans l'appel par tout autre nom de table.

Comment cela marche-t-il?

  • 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 sont évaluées selon le même type fourni lors 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. Nous remettons donc une valeur NULL, transtypée en type de table.

  • 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 sous la forme type d'identifiant d'objet regtype . Lorsqu'ils sont automatiquement convertis en text, les identificateurs sont automatiquement entre guillemets doubles et qualifiés de schéma si nécessaire. Par conséquent, l'injection SQL n'est pas possible. Cela peut même traiter les noms de table qualifiés par le schéma quote_ident() échouerait .

70
Erwin Brandstetter

Vous voudrez probablement retourner un curseur . Essayez quelque chose comme ça (je ne l'ai pas essayé):

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
bpgergo

Je suis désolé de le dire, mais votre question n'est pas très claire. Cependant, ci-dessous, vous trouverez un exemple autonome de création et d'utilisation d'une fonction qui renvoie une variable de curseur. J'espère que ça aide !

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;
1
user272735
# 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() "
0
Yordan Georgiev