web-dev-qa-db-fra.com

Comment utiliser les paramètres de fonction en SQL dynamique avec EXECUTE?

J'ai écrit une fonction PL/pgSQL dans PostgreSQL 9.5. Il compile bien mais quand je l'appelle depuis pgAdmin3 cela me donne une erreur. Il semble que la requête dynamique avec des colonnes à remplacer par les paramètres passés dans la fonction ne fonctionne pas.

Voici ma fonction:

CREATE OR REPLACE FUNCTION insertRecordsForNotification(username text, state     text, district text, organizationId text, bloodGroup text, status text,   approveRejectStatus text, emailSubject text, emailBody text, notificationStatus text) RETURNS boolean AS $$
DECLARE
id int;
r moyadev.user%rowtype;
_where text :=
  concat_ws(' AND '
    , CASE WHEN state IS NOT NULL THEN 'state = $2' END
    , CASE WHEN district IS NOT NULL THEN 'district = $3' END
    , CASE WHEN bloodGroup IS NOT NULL THEN 'bloodGroup = $5' END
    , CASE WHEN status IS NOT NULL THEN 'status = $6' END
    , CASE WHEN approveRejectStatus IS NOT NULL THEN 'approve_reject_status  = $7' END);   
 _sql text := 'INSERT INTO moyadev.notification_email_details (id,     youth_enrollment_id, youth_email, email_subject, email_body, status, attempt,  sent_date, last_updated_by, last_updated) SELECT uuid_generate_v4(), id, email,  $8, $9, $10, null, null,$1, now() FROM moyadev.youth_enrollment';

 BEGIN

SELECT * into r FROM moyadev.user u where u.user_key=$1;


if (r.level='DISTRICT') then
_where := _where || ' AND ' || 'district=r.district' || ' AND ' ||      'state=r.state' || ' AND ' || 'fk_id=r.fk_id';
elseif (r.level='STATE') then
_where := _where || ' AND ' || 'state=r.state' || ' AND ' ||   'fk_id=r.fk_id';
 elseif (r.level='NATIONAL') then
 _where := _where || ' AND ' || 'fk_id=r.fk_id';
 elseif (r.level='UNIT') then
_ where := _where || ' AND ' || 'district=r.district' || ' AND ' ||    'state=r.state' || ' AND ' || 'fk_id=r.fk_id';
end if;

IF _where <> '' THEN
_sql := _sql || ' WHERE ' || _where;
EXECUTE format(_sql);
END IF;

raise notice 'sql: %', _sql;

RETURN 'TRUE';

END; 
$$ LANGUAGE PLPGSQL;

Il compile bien mais donne l'erreur ci-dessous lorsque je l'appelle en utilisant la commande ci-dessous:

select insertRecordsForNotification('[email protected]',null,null,null,null,'ACTIVE','APPROVED','test email','test email','PENDING');
ERROR: there is no parameter $8
SQL state: 42P02
Context: PL/pgSQL function insertrecordsfornotification(text,text,text,text,text,text,text,text,text,text) line 39 at EXECUTE

Comment utiliser correctement les valeurs des paramètres?

6
Pushpendra Pal

Vous confondez deux ou trois choses. Pour passer valeurs à EXECUTE, utilisez la clause USING. Vous n'avez pas besoin de format() ici.

CREATE OR REPLACE FUNCTION insert_records_for_notification(
        _username text
      , _state text
      , _district text
      , _bloodgroup text
      , _status text
      , _approverejectstatus text
      , _emailsubject text
      , _emailbody text
      , _notificationstatus text)
  RETURNS boolean AS
$func$
DECLARE
   r      moyadev.user%rowtype;
   _where text;
   _sql   text :=
     'INSERT INTO moyadev.notification_email_details (id, youth_enrollment_id, youth_email, email_subject, email_body, status, attempt,sent_date, last_updated_by, last_updated)
      SELECT uuid_generate_v4(), id, email, $7, $8, $9, null, null,$1, now()
      FROM   moyadev.youth_enrollment';
BEGIN
   SELECT * INTO r FROM moyadev.user u WHERE u.user_key = _username;

   _where := concat_ws(' AND '
       , CASE WHEN state               IS NOT NULL THEN 'state = $2'                  END
       , CASE WHEN district            IS NOT NULL THEN 'district = $3'               END
       , CASE WHEN bloodGroup          IS NOT NULL THEN 'bloodgroup = $4'             END
       , CASE WHEN status              IS NOT NULL THEN 'status = $5'                 END
       , CASE WHEN approveRejectStatus IS NOT NULL THEN 'approve_reject_status  = $6' END
       , CASE r.level
            WHEN 'DISTRICT' THEN 'district = $10 AND state = $11 AND fk_id = $12'
            WHEN 'UNIT'     THEN 'district = $10 AND state = $11 AND fk_id = $12'
            WHEN 'STATE'    THEN 'state = $11 AND fk_id = $12'
            WHEN 'NATIONAL' THEN 'fk_id = $12'
         END);

   IF _where <> '' THEN
      _sql := _sql || ' WHERE ' || _where;

      EXECUTE _sql
      USING   $1, $2, $3, $4, $5, $6, $7, $8, $9, r.district, r.state, r.fk_id;
   END IF;

   RAISE NOTICE 'sql: %', _sql;

   RETURN true;  -- boolean!
END
$func$  LANGUAGE plpgsql;

Points majeurs

  • Faites pas concaténez les valeurs des paramètres dans les chaînes SQL. Très fastidieux, lent, sujet aux erreurs et ouvert à l'injection SQL. Passez plutôt values à EXECUTE avec la clause USING. En relation:

  • J'ai supprimé la variable inutilisée id int; et le paramètre inutilisé organizationId text. Référence ordinale adaptée ($n) en conséquence.

  • Ne confondez pas le $n notation dans EXECUTE (reportez-vous aux éléments de la clause USING) avec $n notation dans le corps de la fonction (voir les paramètres de la fonction)! En relation:

  • Simplifiez votre logique pour concaténer la clause WHERE. Il y avait des erreurs de casse de coin: si l'affectation initiale aboutissait à une chaîne vide, vous commenciez par AND - une erreur de syntaxe.

  • Adoptez une convention de dénomination qui évite les conflits de dénomination. Les noms de paramètres sont visibles dans toutes les instructions de la fonction (mais pas dans EXECUTE!). N'utilisez pas de noms de variables en conflit avec les noms de colonnes. Une convention courante consiste à ajouter les noms de paramètres et de variables avec _.

  • Mon conseil est d'éviter les identifiants à casse mixte dans Postgres, en particulier lors de l'utilisation de SQL dynamique.

Réponse connexe sur SO:

11
Erwin Brandstetter