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

Tester pour null dans la fonction avec des paramètres variables

Je ne suis pas d'accord avec certains des conseils des autres réponses. Cela peut être fait avec PL/pgSQL et je pense que c'est généralement de loin supérieur à l'assemblage de requêtes dans une application cliente. C'est plus rapide et plus propre et l'application n'envoie que le strict minimum sur le fil dans les demandes. Les instructions SQL sont enregistrées dans la base de données, ce qui facilite la maintenance - à moins que vous ne souhaitiez collecter toute la logique métier dans l'application cliente, cela dépend de l'architecture générale.

Fonction PL/pgSQL avec SQL dynamique

CREATE OR REPLACE FUNCTION func(
      _ad_nr       int  = NULL
    , _ad_nr_extra text = NULL
    , _ad_info     text = NULL
    , _ad_postcode text = NULL
    , _sname       text = NULL
    , _pname       text = NULL
    , _cname       text = NULL)
  RETURNS TABLE(id int, match text, score int, nr int, nr_extra text
              , info text, postcode text, street text, place text
              , country text, the_geom geometry)
  LANGUAGE plpgsql AS
$func$
BEGIN
   -- RAISE NOTICE '%', -- for debugging
   RETURN QUERY EXECUTE concat(
   $$SELECT a.id, 'address'::text, 1 AS score, a.ad_nr, a.ad_nr_extra
        , a.ad_info, a.ad_postcode$$

   , CASE WHEN (_sname, _pname, _cname) IS NULL THEN ', NULL::text' ELSE ', s.name' END  -- street
   , CASE WHEN (_pname, _cname) IS NULL         THEN ', NULL::text' ELSE ', p.name' END  -- place
   , CASE WHEN _cname IS NULL                   THEN ', NULL::text' ELSE ', c.name' END  -- country
   , ', a.wkb_geometry'

   , concat_ws('
   JOIN   '
   , '
   FROM   "Addresses" a'
   , CASE WHEN NOT (_sname, _pname, _cname) IS NULL THEN '"Streets"   s ON s.id = a.street_id' END
   , CASE WHEN NOT (_pname, _cname) IS NULL         THEN '"Places"    p ON p.id = s.place_id' END
   , CASE WHEN _cname IS NOT NULL                   THEN '"Countries" c ON c.id = p.country_id' END
   )

   , concat_ws('
   AND    '
      , '
   WHERE  TRUE'
      , CASE WHEN $1 IS NOT NULL THEN 'a.ad_nr = $1' END
      , CASE WHEN $2 IS NOT NULL THEN 'a.ad_nr_extra = $2' END
      , CASE WHEN $3 IS NOT NULL THEN 'a.ad_info = $3' END
      , CASE WHEN $4 IS NOT NULL THEN 'a.ad_postcode = $4' END
      , CASE WHEN $5 IS NOT NULL THEN 's.name = $5' END
      , CASE WHEN $6 IS NOT NULL THEN 'p.name = $6' END
      , CASE WHEN $7 IS NOT NULL THEN 'c.name = $7' END
   )
   )
   USING $1, $2, $3, $4, $5, $6, $7;
END
$func$;

Appel :

SELECT * FROM func(1, '_ad_nr_extra', '_ad_info', '_ad_postcode', '_sname');

SELECT * FROM func(1, _pname := 'foo');

Étant donné que tous les paramètres de fonction ont des valeurs par défaut, vous pouvez utiliser positionnel notation, nommée notation ou mixte notation à votre choix dans l'appel de fonction. Voir :

  • Fonctions avec un nombre variable de paramètres d'entrée

Plus d'explications sur les bases du SQL dynamique :

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

Le concat() La fonction est instrumentale pour la construction de la chaîne. Il a été introduit avec Postgres 9.1.

Le ELSE branche d'un CASE l'instruction par défaut est NULL lorsqu'il n'est pas présent. Simplifie le code.

Le USING clause pour EXECUTE rend l'injection SQL impossible car les valeurs sont transmises en tant que valeurs et permet d'utiliser directement les valeurs des paramètres, exactement comme dans les instructions préparées.

NULL les valeurs sont utilisées pour ignorer les paramètres ici. Ils ne sont pas réellement utilisés pour effectuer des recherches.

Vous n'avez pas besoin de parenthèses autour du SELECT avec RETURN QUERY .

Fonction SQL simple

Vous pourriez faites-le avec une fonction SQL simple et évitez le SQL dynamique. Dans certains cas, cela peut être plus rapide. Mais je ne m'y attendrais pas dans ce cas . La planification de la requête sans jointures ni prédicats inutiles produit généralement de meilleurs résultats. Le coût de planification d'une requête simple comme celle-ci est presque négligeable.

CREATE OR REPLACE FUNCTION func_sql(
     _ad_nr       int  = NULL
   , _ad_nr_extra text = NULL
   , _ad_info     text = NULL
   , _ad_postcode text = NULL
   , _sname       text = NULL
   , _pname       text = NULL
   , _cname       text = NULL)
  RETURNS TABLE(id int, match text, score int, nr int, nr_extra text
              , info text, postcode text, street text, place text
              , country text, the_geom geometry)
  LANGUAGE sql AS 
$func$
SELECT a.id, 'address' AS match, 1 AS score, a.ad_nr, a.ad_nr_extra
     , a.ad_info, a.ad_postcode
     , s.name AS street, p.name AS place
     , c.name AS country, a.wkb_geometry
FROM   "Addresses"      a
LEFT   JOIN "Streets"   s ON s.id = a.street_id
LEFT   JOIN "Places"    p ON p.id = s.place_id
LEFT   JOIN "Countries" c ON c.id = p.country_id
WHERE ($1 IS NULL OR a.ad_nr = $1)
AND   ($2 IS NULL OR a.ad_nr_extra = $2)
AND   ($3 IS NULL OR a.ad_info = $3)
AND   ($4 IS NULL OR a.ad_postcode = $4)
AND   ($5 IS NULL OR s.name = $5)
AND   ($6 IS NULL OR p.name = $6)
AND   ($7 IS NULL OR c.name = $7)
$func$;

Appel identique.

Pour ignorer efficacement les paramètres avec NULL valeurs :

($1 IS NULL OR a.ad_nr = $1)

Pour utiliser réellement les valeurs NULL comme paramètres , utilisez cette construction à la place :

($1 IS NULL AND a.ad_nr IS NULL OR a.ad_nr = $1)  -- AND binds before OR

Cela permet également des index à utiliser.
Dans le cas présent, remplacez toutes les instances de LEFT JOIN avec JOIN .

db<>jouez ici - avec démo simple pour toutes les variantes.
Ancien sqlfiddle

En passant

  • N'utilisez pas name et id comme noms de colonne. Ils ne sont pas descriptifs et lorsque vous rejoignez un groupe de tables (comme vous le faites pour a lot dans une base de données relationnelle), vous vous retrouvez avec plusieurs colonnes toutes nommées name ou id , et doivent attacher des alias pour trier le désordre.

  • Veuillez formater votre SQL correctement, au moins lorsque vous posez des questions publiques. Mais faites-le aussi en privé, pour votre propre bien.