sql >> Database >  >> RDS >> PostgreSQL

Refactor een PL/pgSQL-functie om de uitvoer van verschillende SELECT-query's te retourneren

Dynamische SQL en RETURN typ


U wilt dynamische SQL uitvoeren . In principe is dat eenvoudig in plpgsql met behulp van EXECUTE . Je hebt geen nodig een cursor. In feite ben je meestal beter af zonder expliciete cursors.

Het probleem waar u tegenaan loopt:u wilt records van een nog niet gedefinieerd type retourneren . Een functie moet zijn retourtype declareren in de RETURNS clausule (of met OUT of INOUT parameters). In jouw geval zou je terug moeten vallen op anonieme gegevens, omdat nummer , namen en typen van de geretourneerde kolommen variëren. Vind ik leuk:

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

Dit is echter niet bijzonder handig. U moet bij elke oproep een kolomdefinitielijst opgeven. Vind ik leuk:

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

Maar hoe zou u dit zelfs doen als u de kolommen niet van tevoren kent?
U kunt minder gestructureerde documentgegevenstypen gebruiken, zoals json , jsonb , hstore of xml . Zie:

  • Hoe bewaar ik een gegevenstabel in de database?

Maar laten we voor het doel van deze vraag aannemen dat u zoveel mogelijk individuele, correct getypte en benoemde kolommen wilt retourneren.

Eenvoudige oplossing met vast retourtype

De kolom datahora lijkt een gegeven te zijn, neem ik aan dat gegevenstype timestamp en dat er altijd nog twee kolommen zijn met verschillende namen en gegevenstypes.

Namen we zullen afzien van generieke namen in het retourtype.
Typen we zullen het ook opgeven en alles naar text casten sinds elke gegevenstype kan worden gecast naar 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$;

De variabelen _sensors en _type kunnen in plaats daarvan invoerparameters zijn.

Let op de RETURNS TABLE clausule.

Let op het gebruik van RETURN QUERY EXECUTE . Dat is een van de elegantere manieren om rijen uit een dynamische zoekopdracht te retourneren.

Ik gebruik een naam voor de functieparameter, alleen om de USING clausule van RETURN QUERY EXECUTE minder verwarrend. $1 in de SQL-string verwijst niet naar de functieparameter maar naar de waarde doorgegeven met de USING clausule. (Beide zijn toevallig $1 in hun respectievelijke bereik in dit eenvoudige voorbeeld.)

Let op de voorbeeldwaarde voor _sensors :elke kolom wordt gecast om text te typen .

Dit soort code is erg kwetsbaar voor SQL-injectie . Ik gebruik quote_ident() ertegen te beschermen. Een paar kolomnamen samenvoegen in de variabele _sensors voorkomt het gebruik van quote_ident() (en is meestal een slecht idee!). Zorg ervoor dat er op een andere manier geen slechte dingen in kunnen zitten, bijvoorbeeld door de kolomnamen afzonderlijk door quote_ident() te laten lopen in plaats van. Een VARIADIC parameter komt in me op ...

Eenvoudiger sinds PostgreSQL 9.1

Met versie 9.1 of hoger kunt u format() . gebruiken om verder te vereenvoudigen:

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;

Nogmaals, individuele kolomnamen zouden correct kunnen worden ontsnapt en zou de schone manier zijn.

Variabel aantal kolommen met hetzelfde type

Nadat uw vraag is bijgewerkt, lijkt het erop dat uw retourtype

. heeft
  • een variabel getal kolommen
  • maar alle kolommen van hetzelfde type double precision (alias float8 )

Gebruik een ARRAY typ in dit geval om een ​​variabel aantal waarden te nesten. Bovendien retourneer ik een array met kolomnamen:

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

Verschillende complete tafeltypes

Om daadwerkelijk alle kolommen van een tabel terug te geven , is er een eenvoudige, krachtige oplossing met een polymorf type :

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

Bel (belangrijk!):

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

Vervang pcdmet in het gesprek met een andere tabelnaam.

Hoe werkt dit?

anyelement is een pseudo-gegevenstype, een polymorf type, een tijdelijke aanduiding voor elk niet-array-gegevenstype. Alle exemplaren van anyelement in de functie evalueren naar hetzelfde type dat tijdens runtime is opgegeven. Door een waarde van een gedefinieerd type als argument aan de functie te geven, definiëren we impliciet het retourtype.

PostgreSQL definieert automatisch een rijtype (een samengesteld gegevenstype) voor elke gemaakte tabel, dus er is een goed gedefinieerd type voor elke tabel. Dit omvat tijdelijke tabellen, wat handig is voor ad-hocgebruik.

Elk type kan NULL zijn . Dien een NULL in waarde, cast naar het tabeltype:NULL::pcdmet .

Nu retourneert de functie een goed gedefinieerd rijtype en kunnen we SELECT * FROM data_of() gebruiken om de rij te ontleden en individuele kolommen te krijgen.

pg_typeof(_tbl_type) geeft de naam van de tabel terug als object-ID type regtype . Wanneer automatisch geconverteerd naar text , ID's worden automatisch dubbele aanhalingstekens en schema-gekwalificeerd indien nodig, automatische verdediging tegen SQL-injectie. Dit kan zelfs omgaan met schema-gekwalificeerde tabelnamen waarbij quote_ident() zou mislukken. Zie:

  • Tabelnaam als een PostgreSQL-functieparameter


  1. Gebruikers alleen toegang geven tot bepaalde tabellen in mijn inhoudsprovider

  2. Hoe zie ik welke tekenset een MySQL-database / tabel / kolom is?

  3. ORA-00979 geen groep op uitdrukking

  4. Zijn geneste transacties toegestaan ​​in MySQL?