将公式存储在表中并在函数中使用该公式


10

我有一个PostgreSQL 9.1数据库,其中部分处理代理佣金。每个代理商都有自己的计算公式,他们可以得到多少佣金。我有一个函数来生成每个代理应获得的佣金数量,但是随着代理数量的增加,它变得无法使用。被迫做一些非常长的case语句和重复代码,这使我的功能变得非常庞大。

所有公式都有常量变量:

d ..该月工作的天数
r ..新节点累积
l ..忠诚度得分
s ..子代理人佣金
b ..基本利率
我..获得的收入

公式可以是:

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

每个代理商与人力资源部门协商付款方式。因此,我可以将公式存储在代理表中,然后像一个小的函数一样,仅从表中获取公式并将其转换为值并计算金额吗?

Answers:


6

准备

您的公式如下所示:

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

我将用$n符号替换变量,以便可以直接在plpgsql中将其替换为值EXECUTE(请参见下文):

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

您可以额外存储原始公式(供人眼使用),也可以使用以下表达式动态生成此格式:

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

只要确保您的翻译是正确的即可。regexp表达式的一些解释:

\ m ..仅在单词开头
匹配\ M ..仅在单词结尾匹配

第四个参数'g'..全局替换

核心功能

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; 

呼叫:

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

返回值:

29.6000000000000000

要点

  • 该功能以6值参数formula text为第7位。我将公式放在最后,因此我们可以使用$1 .. $6代替$2 .. $7。仅出于可读性考虑。
    我认为合适的是为值分配了数据类型。分配适当的类型(以执行基本的健全性检查)或将其全部设置为numeric

  • 通过USING子句传递用于动态执行的值。这样可以避免来回转换,并使一切变得更简单,更安全,更快。

  • 我使用一个OUT参数是因为它更优雅,并且语法更短。RETURN不需要final ,将自动返回OUT参数的值。

  • 考虑@Chris 撰写的有关安全性的讲座以及手册中的“安全地编写安全定义函数”一章。在我的设计中,注射点是配方本身。

  • 您可以对某些参数使用默认值,以进一步简化调用。


5

请仔细阅读有关安全方面的注意事项。本质上,您试图在函数中注入任意SQL。因此,您需要在具有严格限制的权限的用户下运行此程序。

  1. 创建一个用户并从中撤消所有权限。请勿在同一数据库中向public授予权限。

  2. 创建一个函数来评估表达式,进行创建security definer并将所有者更改为该受限用户。

  3. 预处理表达式,然后将其传递到上面创建的eval()函数。您可以根据需要在其他功能中执行此操作,

再次注意,这具有严重的安全隐患。

编辑:简短的示例代码(未经测试,但如果您遵循文档,应该可以使您到达那里):

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.

“使它成为安全定义器”确实令人困惑,您能解释一下吗?
jcolebrand

1
PostgreSQL有两种可以运行功能的安全模式。默认为“安全调用程序”。SECURITY DEFINER的意思是“在函数所有者的安全上下文中运行”,类似于* nix上的SETUID位。要创建函数安全定义程序,您可以在函数声明(CREATE FUNCTION foo(text) returns text IMMUTABLE LANGUAGE SQL SECURITY DEFINER AS $$...)中指定,也可以ALTER FUNCTION foo(text) SECURITY DEFINER
Chris Travers

哦,这是特定的PG lino。知道了 Shoulda在答案中使用了反引号;-)
jcolebrand

@ChrisTravers我期待一些示例代码来评估一个公式,即a+b存储在表中的文本类型列中,然后我有一个函数,foo(a int, b int,formula text)如果得到的公式是a + b,我如何使该函数实际执行a + b而不是我必须对所有可能的公式使用非常长的case语句,并在所有段中重复执行代码?
indago

1
@indago,出于安全方面的考虑,我认为您希望将此分为两层。第一个是插值层。您可以在PostgreSQL中使用正则表达式来执行此操作。在较低级别,您基本上是在监狱的SQL函数中运行它。但是,如果确实要这样做,则确实需要非常注意安全性,并且还必须特别注意返回值。如果不了解更多信息,那么很难使用足够的代码,但是会修改答案。
克里斯·特拉弗斯

2

除了存储公式然后执行它(如Chris提到的那样,存在安全性问题)的一种替代方法是有一个单独的表formula_steps,该表基本上将包含变量和运算符以及执行它们的顺序。这将需要更多的工作,但会更加安全。该表可能如下所示:

公式步骤
-------------
  Formula_Step_ID
  Formula_ID(FK,由代理商表引用)
  输入_1
  输入_2
  运算符(如果不想直接存储运算符,也可以是允许的运算符表的ID)
  顺序

另一个选择是使用一些第三方库/工具来评估数学表达式。这将使您的数据库不太容易受到SQL注入的攻击,但是现在您已经将可能的安全性问题转移到了外部工具(这仍然很安全)。


最后的选择是编写(或下载)评估数学表达式的过程。有解决此问题的已知算法,因此在网上查找信息应该不难。


1
+1为第三个选项。如果所有可能的输入都已知,则对每个输入进行硬编码,并将它们(如果需要)替换为以文本形式存储的公式,然后使用库例程评估该算法。消除了SQL注入风险。
乔尔·布朗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.