Speichern Sie eine Formel in einer Tabelle und verwenden Sie die Formel in einer Funktion

10

Ich habe eine PostgreSQL 9.1-Datenbank, in der ein Teil davon Agentenprovisionen verarbeitet. Jeder Agent hat seine eigene Berechnungsformel, wie viel Provision er erhält. Ich habe eine Funktion, um die Höhe der Provision zu generieren, die jeder Agent erhalten sollte, aber es wird unmöglich, sie zu verwenden, wenn die Anzahl der Agenten wächst. Ich bin gezwungen, einige extrem lange case-Anweisungen zu machen und Code zu wiederholen, was meine Funktion sehr groß gemacht hat.

Alle Formeln haben konstante Variablen:

d .. Tage arbeiteten in diesem Monat
r .. neue Knoten erworben
l .. Treuepunktzahl
s .. Subagentenprovision
b .. Basiszinssatz
i .. Umsatz gewonnen

Die Formel kann ungefähr so ​​lauten:

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

Jeder Agent verhandelt die Zahlungsformel mit der Personalabteilung. Kann ich die Formel in der Agententabelle speichern und dann wie eine kleine Funktion die Formel aus der Tabelle abrufen, mit Werten übersetzen und den Betrag berechnen?

Indago
quelle

Antworten:

6

Bereiten

Ihre Formeln sehen folgendermaßen aus:

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

Ich würde die Variablen durch $nNotation ersetzen, damit sie direkt in plpgsql durch Werte ersetzt werden können EXECUTE(siehe unten):

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

Sie können Ihre Originalformeln zusätzlich (für das menschliche Auge) speichern oder dieses Formular dynamisch mit einem Ausdruck wie dem folgenden generieren:

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');

Stellen Sie einfach sicher, dass Ihre Übersetzung solide ist. Einige Erklärungen für die regulären Ausdrücke :

\ m .. stimmt nur am Anfang eines Wortes
überein \ M .. stimmt nur am Ende eines Wortes überein

4. Parameter 'g'.. global ersetzen

Hauptfunktion

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; 

Anruf:

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

Kehrt zurück:

29.6000000000000000

Hauptpunkte

  • Die Funktion nimmt 6 Wert Parameter und formula textals 7 .. Ich habe die Formel zuletzt gesetzt, damit wir $1 .. $6stattdessen verwenden können $2 .. $7. Nur aus Gründen der Lesbarkeit.
    Ich habe den Werten nach eigenem Ermessen Datentypen zugewiesen. Weisen Sie die richtigen Typen zu (um grundlegende Sanitätsprüfungen durchzuführen) oder machen Sie einfach alle numeric:

  • Übergeben Sie Werte für die dynamische Ausführung mit der USINGKlausel. Dies vermeidet das Hin- und Herwerfen und macht alles einfacher, sicherer und schneller.

  • Ich benutze einen OUTParameter, weil das eleganter ist und eine kürzere, klarere Syntax ermöglicht. Ein Finale RETURNwird nicht benötigt, der Wert der OUT-Parameter wird automatisch zurückgegeben.

  • Beachten Sie den Vortrag über Sicherheit von @Chris und das Kapitel "Sicheres Schreiben von SECURITY DEFINER-Funktionen" im Handbuch. In meinem Design ist der einzige Injektionspunkt die Formel selbst.

  • Sie können für einige Parameter Standardeinstellungen verwenden , um den Aufruf weiter zu vereinfachen.

Erwin Brandstetter
quelle
5

Bitte lesen Sie dies sorgfältig aus Sicherheitsgründen. Im Wesentlichen versuchen Sie, beliebiges SQL in Ihre Funktionen einzufügen. Folglich muss dies unter einem Benutzer mit stark eingeschränkten Berechtigungen ausgeführt werden.

  1. Erstellen Sie einen Benutzer und widerrufen Sie ihm alle Berechtigungen. Erteilen Sie der Öffentlichkeit keine Berechtigungen in derselben Datenbank wie Sie.

  2. Erstellen Sie eine Funktion, um den Ausdruck auszuwerten, zu erstellen security definerund den Eigentümer in diesen eingeschränkten Benutzer zu ändern.

  3. Verarbeiten Sie den Ausdruck vor und übergeben Sie ihn an die oben erstellte Funktion eval (). Sie können dies in einer anderen Funktion tun, wenn Sie müssen,

Beachten Sie erneut, dass dies schwerwiegende Auswirkungen auf die Sicherheit hat.

Bearbeiten: Kurzer Beispielcode (ungetestet, sollte Sie aber dorthin bringen, wenn Sie den Dokumenten folgen):

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.
Chris Travers
quelle
"make it security definer" ist wirklich verwirrend, können Sie erklären?
Jcolebrand
1
PostgreSQL verfügt über zwei Sicherheitsmodi, unter denen eine Funktion ausgeführt werden kann. SECURITY INVOKER ist die Standardeinstellung. SECURITY DEFINER bedeutet "Mit dem Sicherheitskontext des Eigentümers der Funktion ausführen", ähnlich dem SETUID-Bit auf * nix. Um einen Funktionssicherheits-Definierer zu erstellen, können Sie dies in der Funktionsdeklaration ( CREATE FUNCTION foo(text) returns text IMMUTABLE LANGUAGE SQL SECURITY DEFINER AS $$...) angeben oderALTER FUNCTION foo(text) SECURITY DEFINER
Chris Travers
Oh, das ist also ein spezifischer PG Lino. Erwischt. Sollte Backticks in der Antwort verwendet werden ;-)
jcolebrand
@ChrisTravers Ich hatte erwartet, dass ein Beispielcode eine Formel auswertet, dh a+bin einer Textspalte in einer Tabelle gespeichert ist. Dann habe ich eine Funktion, foo(a int, b int,formula text)wenn die Formel a + b ist. Wie kann ich die Funktion dazu bringen, tatsächlich a + b anstelle von auszuführen? Muss ich eine sehr lange case-Anweisung für alle möglichen Formeln haben und den Code in allen Segmenten wiederholen?
Indago
1
@indago, ich denke, Sie möchten dies aus Sicherheitsgründen in zwei Ebenen aufteilen. Die erste ist eine Interpolationsschicht. Sie können dazu Regexes in PostgreSQL verwenden. In der unteren Ebene führen Sie dies grundsätzlich in einer inhaftierten SQL-Funktion aus. Sie müssen jedoch sehr genau auf die Sicherheit achten, wenn Sie dies tun möchten, und Sie müssen auch genau auf die Rückgabewerte achten. Ohne viel mehr zu wissen, ist es schwierig, viel mit Samople-Code zu tun, aber die Antwort wird geändert.
Chris Travers
2

Eine Alternative zum Speichern und anschließenden Ausführen der Formel (die, wie Chris erwähnte, Sicherheitsprobleme aufweist ) wäre eine separate Tabelle, formula_stepsdie im Wesentlichen die Variablen und Operatoren sowie die Reihenfolge enthält, in der sie ausgeführt werden. Dies wäre etwas mehr Arbeit, aber sicherer. Die Tabelle könnte folgendermaßen aussehen:

formel_schritte
-------------
  formel_schritt_id
  formel_id (FK, auf die in der Agententabelle verwiesen wird)
  input_1
  input_2
  Operator (kann auch eine ID für eine Tabelle zulässiger Operatoren sein, wenn Sie Operatorsymbole nicht direkt speichern möchten)
  Reihenfolge

Eine andere Möglichkeit wäre die Verwendung einer Bibliothek / eines Tools eines Drittanbieters zur Auswertung mathematischer Ausdrücke. Dies würde Ihre Datenbank weniger anfällig für SQL-Injection machen, aber jetzt haben Sie die möglichen Sicherheitsprobleme auf Ihr externes Tool verlagert (das möglicherweise immer noch ziemlich sicher ist).


Die letzte Option wäre das Schreiben (oder Herunterladen) einer Prozedur, die mathematische Ausdrücke auswertet. Es gibt bekannte Algorithmen für dieses Problem, daher sollte es nicht schwierig sein, Informationen online zu finden.

FrustratedWithFormsDesigner
quelle
1
+1 für die dritte Option. Wenn alle potenziellen Eingaben bekannt sind, codieren Sie eine Auswahl der einzelnen Eingaben fest und setzen Sie sie (falls erforderlich) in die als Text gespeicherte Formel ein. Verwenden Sie dann eine Bibliotheksroutine, um die Arithmetik auszuwerten. SQL-Injection-Risiko beseitigt.
Joel Brown