PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Test auf null in der Funktion mit variierenden Parametern

Ich bin mit einigen Ratschlägen in anderen Antworten nicht einverstanden. Dies kann mit PL/pgSQL erfolgen und ich denke, es ist meistens weit überlegen zum Zusammenstellen von Abfragen in einer Client-Anwendung. Es ist schneller und sauberer und die App sendet nur das Nötigste an Anfragen über das Kabel. SQL-Anweisungen werden in der Datenbank gespeichert, was die Wartung erleichtert - es sei denn, Sie möchten die gesamte Geschäftslogik in der Client-Anwendung sammeln, dies hängt von der allgemeinen Architektur ab.

PL/pgSQL-Funktion mit dynamischem SQL

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

Aufruf:

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

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

Da alle Funktionsparameter Standardwerte haben, können Sie positional verwenden Notation, benannt Notation oder gemischt Notation Ihrer Wahl im Funktionsaufruf. Siehe:

  • Funktionen mit variabler Anzahl von Eingabeparametern

Weitere Erläuterungen zu den Grundlagen von dynamischem SQL:

  • Refaktorisieren Sie eine PL/pgSQL-Funktion, um die Ausgabe verschiedener SELECT-Abfragen zurückzugeben

Der concat() Die Funktion ist maßgeblich für den Aufbau der Zeichenfolge. Es wurde mit Postgres 9.1 eingeführt.

Das ELSE Zweig eines CASE -Anweisung ist standardmäßig NULL wenn nicht vorhanden. Vereinfacht den Code.

Der USING Klausel für EXECUTE macht SQL-Injection unmöglich, da Werte als Werte übergeben werden und ermöglicht die direkte Verwendung von Parameterwerten, genau wie in vorbereiteten Anweisungen.

NULL Werte werden verwendet, um Parameter hier zu ignorieren. Sie werden nicht wirklich für die Suche verwendet.

Sie brauchen keine Klammern um SELECT mit RETURN QUERY .

Einfache SQL-Funktion

Sie könnten tun Sie es mit einer einfachen SQL-Funktion und vermeiden Sie dynamisches SQL. In einigen Fällen kann dies schneller sein. Aber das würde ich in diesem Fall nicht erwarten . Das Planen der Abfrage ohne unnötige Verknüpfungen und Prädikate führt normalerweise zu den besten Ergebnissen. Die Planungskosten für eine einfache Abfrage wie diese sind nahezu vernachlässigbar.

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

Identischer Aufruf.

Um Parameter mit NULL effektiv zu ignorieren Werte :

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

Um tatsächlich NULL-Werte als Parameter zu verwenden verwenden Sie stattdessen dieses Konstrukt:

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

Dies ermöglicht auch Indizes verwendet werden.
Ersetzen Sie im vorliegenden Fall alle Instanzen von LEFT JOIN mit JOIN .

db<>hier fummeln - mit einfacher Demo für alle Varianten.
Altes sqlfiddle

Nebenbei

  • Verwenden Sie nicht name und id als Spaltennamen. Sie sind nicht beschreibend und wenn Sie sich einer Reihe von Tischen anschließen (wie Sie es bei a lot tun in einer relationalen Datenbank), erhalten Sie am Ende mehrere Spalten mit dem Namen name oder id , und müssen Aliase anhängen, um das Durcheinander zu sortieren.

  • Bitte formatieren Sie Ihr SQL richtig, zumindest wenn Sie öffentliche Fragen stellen. Aber tun Sie es auch privat, zu Ihrem eigenen Besten.