Archivia una formula in una tabella e usa la formula in una funzione


10

Ho un database PostgreSQL 9.1 in cui parte gestisce le commissioni degli agenti. Ogni agente ha la propria formula di calcolo della commissione che riceve. Ho una funzione per generare la quantità di provvigione che ogni agente dovrebbe ottenere, ma sta diventando impossibile da usare con l'aumentare del numero di agenti. Sono costretto a fare alcune dichiarazioni di casi estremamente lunghi e codice ripetuto, il che ha reso la mia funzione molto grande.

Tutte le formule hanno variabili costanti:

d .. giorni lavorati quel mese
r .. nuovi nodi accuired
l .. punteggio fedeltà
s. commissione subagente
b .. tariffa base
io ... entrate guadagnate

La formula può essere qualcosa del tipo:

d*b+(l*4+r)+(i/d)+s

Ogni agente negozia la formula di pagamento con il reparto risorse umane. Quindi posso memorizzare la formula nella tabella degli agenti e quindi avere una piccola funzione che ottiene semplicemente la formula dalla tabella e la traduce con valori e calcola l'importo?

Risposte:


6

Preparare

Le tue formule si presentano così:

d*b+(l*4+r)+(i/d)+s

Vorrei sostituire le variabili con la $nnotazione in modo che possano essere sostituite con i valori direttamente in plpgsql EXECUTE(vedi sotto):

$1*$5+($3*4+$2)+($6/$1)+$4

Puoi memorizzare anche le tue formule originali (per l'occhio umano) o generare questo modulo in modo dinamico con un'espressione come:

SELECT regexp_replace(regexp_replace(regexp_replace(
       regexp_replace(regexp_replace(regexp_replace(
      'd*b+(l*4+r)+(i/d)+s'
      , '\md\M', '$1', 'g')
      , '\mr\M', '$2', 'g')
      , '\ml\M', '$3', 'g')
      , '\ms\M', '$4', 'g')
      , '\mb\M', '$5', 'g')
      , '\mi\M', '$6', 'g');

Assicurati solo che la traduzione sia valida. Qualche spiegazione per le espressioni regexp :

\ m .. corrisponde solo all'inizio di una parola
\ M .. corrisponde solo alla fine di una parola

4 ° parametro 'g'.. sostituire a livello globale

Funzione principale

CREATE OR REPLACE FUNCTION f_calc(
    d int         --  days worked that month
   ,r int         --  new nodes accuired
   ,l int         --  loyalty score
   ,s numeric     --  subagent commission
   ,b numeric     --  base rate
   ,i numeric     --  revenue gained
   ,formula text
   ,OUT result numeric
)  RETURNS numeric AS
$func$
BEGIN    
   EXECUTE 'SELECT '|| formula
   INTO   result
   USING  $1, $2, $3, $4, $5, $6;                                          
END
$func$ LANGUAGE plpgsql SECURITY DEFINER IMMUTABLE; 

Chiamata:

SELECT f_calc(1, 2, 3, 4.1, 5.2, 6.3, '$1*$5+($3*4+$2)+($6/$1)+$4');

Ritorna:

29.6000000000000000

Punti importanti

  • La funzione accetta 6 parametri e formula textcome settimo. Ho messo la formula per ultima, quindi possiamo usare al $1 .. $6posto di $2 .. $7. Solo per motivi di leggibilità.
    Ho assegnato i tipi di dati per i valori come ho ritenuto opportuno. Assegna i tipi corretti (per implementare i controlli di integrità di base) o semplicemente eseguili tutti numeric:

  • Passa i valori per l'esecuzione dinamica con la USINGclausola. Questo evita il lancio avanti e indietro e rende tutto più semplice, sicuro e veloce.

  • Uso un OUTparametro perché è più elegante e rende la sintassi più chiara e più breve. RETURNNon è necessario un finale , il valore dei parametri OUT viene restituito automaticamente.

  • Considera la lezione sulla sicurezza di @Chris e il capitolo "Scrivere in sicurezza le funzioni di DEFINER" nel manuale. Nel mio progetto, l'unico punto di iniezione è la formula stessa.

  • È possibile utilizzare i valori predefiniti per alcuni parametri per semplificare ulteriormente la chiamata.


5

Si prega di leggere attentamente le considerazioni relative alla sicurezza. Essenzialmente stai cercando di iniettare SQL arbitrario nelle tue funzioni. Di conseguenza è necessario che questo venga eseguito con un utente con autorizzazioni altamente limitate.

  1. Crea un utente e revoca tutte le autorizzazioni da esso. Non concedere le autorizzazioni al pubblico nello stesso db in cui si esegue questa operazione.

  2. Creare una funzione per valutare l'espressione, crearla security definere modificare il proprietario a quell'utente con restrizioni.

  3. Elaborare in anticipo l'espressione e quindi passarla alla funzione eval () creata in precedenza. Puoi farlo in un'altra funzione se necessario,

Nota di nuovo, questo ha serie implicazioni per la sicurezza.

Modifica: breve codice di esempio (non testato ma dovrebbe arrivare lì se segui i documenti):

CREATE OR REPLACE FUNCTION eval_numeric(text) returns numeric language plpgsql security definer immutable as
$$
declare retval numeric;
begin

execute $e$ SELECT ($1)::numeric$e$ into retval;
return retval;
end;
$$;

ALTER FUNCTION eval_numeric OWNER TO jailed_user;

CREATE OR REPLACE FUNCTION foo(expression text, a numeric, b numeric) returns numeric language sql immutable as $$
select eval(regexp_replace(regexp_replace($1, 'a', $2, 'g'), 'b', '$3', 'g'));
$$; -- can be security invoker, but eval needs to be jailed.

"Renderlo definitivo di sicurezza" è davvero confuso, puoi spiegarlo?
jcolebrand

1
PostgreSQL ha due modalità di sicurezza che una funzione può eseguire come. SECOKITY INVOKER è l'impostazione predefinita. SECURITY DEFINER significa "eseguito con il contesto di sicurezza del proprietario della funzione" un po 'come il bit SETUID su * nix. Per definire un definitore di sicurezza delle funzioni, puoi specificarlo nella dichiarazione di funzione ( CREATE FUNCTION foo(text) returns text IMMUTABLE LANGUAGE SQL SECURITY DEFINER AS $$...) oppure puoiALTER FUNCTION foo(text) SECURITY DEFINER
Chris Travers,

Oh, questo è specifico PG lino. Gotcha. Dovrei usare i backtick nella Risposta ;-)
jcolebrand

@ChrisTravers mi aspettavo un po 'di codice di esempio per valutare una formula cioè a+bè memorizzato in una colonna di tipo di testo in una tabella, quindi ho una funzione foo(a int, b int,formula text)se ottiene la formula è a + b come posso fare la funzione effettivamente fare un + b invece di devo avere una case case molto lunga per tutte le possibili formule e ripetere il codice in tutti i segmenti?
indago,

1
@indago, penso che tu voglia dividerlo in due livelli a causa di problemi di sicurezza. Il primo è uno strato di interpolazione. Per fare questo puoi usare regex in PostgreSQL. Nel livello inferiore, sostanzialmente lo si esegue in una funzione SQL con jailing. Tuttavia, è necessario prestare molta attenzione alla sicurezza se si intende farlo e si deve anche prestare molta attenzione ai valori di ritorno. Senza saperne di più, è difficile fare molto con il codice samople ma modificherà la risposta.
Chris Travers,

2

Un'alternativa alla sola memorizzazione della formula e quindi all'esecuzione (che, come ha detto Chris, ha problemi di sicurezza ) sarebbe quella di avere una tabella separata chiamata formula_stepsche sostanzialmente contenga le variabili e gli operatori e la sequenza in cui vengono eseguite. Questo sarebbe un po 'più di lavoro, ma sarebbe più sicuro. La tabella potrebbe apparire così:

formula_steps
-------------
  formula_step_id
  formula_id (FK, a cui fa riferimento la tabella degli agenti)
  input_1
  INPUT_2
  operatore (potrebbe anche essere un ID per una tabella di operatori consentiti, se non si desidera memorizzare direttamente i simboli dell'operatore)
  sequenza

Un'altra opzione sarebbe quella di utilizzare una libreria / strumento di terze parti per valutare le espressioni matematiche. Ciò renderebbe il tuo database meno vulnerabile all'iniezione di SQL, ma ora hai appena spostato i possibili problemi di sicurezza sul tuo strumento esterno (che potrebbe essere ancora abbastanza sicuro).


L'ultima opzione sarebbe quella di scrivere (o scaricare) una procedura che valuti le espressioni matematiche. Esistono algoritmi noti per questo problema, quindi non dovrebbe essere difficile trovare informazioni online.


1
+1 per la terza opzione. Se tutti gli input potenziali sono noti, codificare una selezione di ciascuno degli input e sostituirli (se e quando necessario) nella formula memorizzata come testo, quindi utilizzare una routine di libreria per valutare l'aritmetica. Rischio di iniezione SQL eliminato.
Joel Brown,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.