是否应将业务逻辑保存在数据库中?

发布时间 2023-08-01 08:46:44作者: abce

 

开源数据库架构师通常不会在数据库中实现业务逻辑。这与许多商业数据库的普遍做法形成了鲜明对比。
在开源数据库中,所有启发式方法都保留在应用层,数据库对数据质量几乎没有影响。在商业数据库中,管理的要求规则由数据库本身处理,而不是依赖软件开发人员对规则的充分认识。

开源世界中的这一立场的部分原因是,过去在MySQL环境中缺乏业务逻辑工具。真正能发挥其应有作用的检查(check)约束仍然是MySQL最近才加入的新功能,而且只出现在8.0.16或更高版本中,这也是升级的另一个原因。以前的MySQL版本会检查语法,但不会执行约束检查所需的工作。
但现在既然有了工具,也许是时候重新审视传统立场了。PostgreSQL很早就具备了下面示例中的功能,MongoDB在这方面也有一些能力。因此,如果你倾向于将业务逻辑转移到数据库层,那么这些功能是存在的。

将不良数据排除在数据库之外,在时间和金钱上的成本都要比将不良数据放入表中后再进行纠正低得多。您可能已经使用了以下一些工具来过滤数据。但在数据成为行或文档之前,你可以更严格地确保数据的原始性。

举例说明,保护数据的方法有很多,但我们将从ENUM和VIEW开始,因为它们在MySQL中已经存在了很长时间。

 

ENUM
ENUM是一个字符串对象,其值是从允许值列表中选择的,在创建表时,列规范中明确枚举了允许值。

ENUM的概念非常好。可以设置要包含在表中的预定义值,踢出不符合这些预定义值的数据,这样数据就符合要求了。MongoDB、MySQL和PostgreSQL都支持ENUM。在下面的示例中,创建的ENUM只接受三个值,并拒绝其他值。

>CREATE TABLE enum_example (id int, is_good ENUM ('yes','no','maybe'));
Query OK, 0 rows affected (0.0188 sec)
>INSERT INTO enum_example (id, is_good) values (1,'yes');
Query OK, 1 row affected (0.0066 sec)
>INSERT INTO enum_example (id, is_good) values (2,'no');
Query OK, 1 row affected (0.0034 sec)
>INSERT INTO enum_example (id, is_good) values (3,'unknown');
ERROR: 1265: Data truncated for column 'is_good' at row 1
>show warnings;
+-------+------+----------------------------------------------+
| Level | Code | Message |
+-------+------+----------------------------------------------+
| Error | 1265 | Data truncated for column 'is_good' at row 1 |
+-------+------+----------------------------------------------+
1 row in set (0.0009 sec)

在上面的示例中,当输入预定义值时,一切都很顺利,而且不出所料,不符合要求的数据行会被直接拒绝。但是,如果我们尝试大家最喜欢的NULL值,会发生什么情况呢?

>INSERT INTO enum_example (id, is_good) values (3,NULL);
Query OK, 1 row affected (0.0040 sec)
>SELECT * FROM enum_example;
+----+---------+
| id | is_good |
+----+---------+
| 1 | yes |
| 2 | no |
| 3 | NULL |
+----+---------+
3 rows in set (0.0027 sec)

NULL并不是我们预定义的值之一,但它却在我们的数据库表中安然无恙。这与我们在逻辑上或对数据所希望的结果不同。将SQL模式设置为STRICT,就可以避免NULL问题。

ENUM不受欢迎的另一个原因是排序很麻烦,因为ENUMS是根据索引号排序的。在上面的示例中,"yes"排序第一,"no"排序第二,"unknown"排序第三。如果这种排序方式不适合你,或者你需要按字母顺序对值进行排序,那就很麻烦了。

>SELECT id, is_good FROM enum_example ORDER BY is_good;
+----+---------+
| id | is_good |
+----+---------+
| 3 | NULL |
| 1 | yes |
| 2 | no |
+----+---------+
3 rows in set (0.0012 sec)

而NULL又做了NULL所做的事情,首先被排序。ENUM的概念非常精妙,但实现起来却令人头疼。这并不意味着你不应该使用它们,但你需要意识到它们的缺点,并做出相应的调整。

 

VIEWS
你可能不认为视图是业务逻辑的一部分,但它们对于混淆数据使其不被窥视却很有价值。视图可以像表一样处理,而且可以通过定义视图,使使用视图的人员的权限远远小于视图的创建者。原始表的访问受限,只有经过授权的用户才能访问,而视图的建立可以让调用者间接访问该表的部分内容。

在下面的示例中,我们有一个带有secret列的表。为清楚起见,省略了限制访问的各种权限。创建该表后,只有选定的账户才能查看该secret列。

>CREATE TABLE secret_stuff (id int, secret int);
Query OK, 0 rows affected (0.0412 sec)
>INSERT INTO secret_stuff (id, secret) VALUES (1,1),(2,2);
Query OK, 2 rows affected (0.0062 sec)

Records: 2 Duplicates: 0 Warnings: 0
>SELECT * FROM secret_stuff;
+----+--------+
| id | secret |
+----+--------+
| 1 | 1 |
| 2 | 2 |
+----+--------+
2 rows in set (0.0011 sec)

然后创建一个视图。该视图的用户不能直接访问secret列,但可以访问表中其他受保护较少的数据。

>CREATE VIEW see_secret AS SELECT id, 'XYZ' FROM secret_stuff;
Query OK, 0 rows affected (0.0084 sec)
>select * from see_secret;
+----+-----+
| id | XYZ |
+----+-----+
| 1 | XYZ |
| 2 | XYZ |
+----+-----+
2 rows in set (0.0045 sec)

表中的数据受到保护,无法直接看到secret列,因为他们无法看到该列的名称,更不用说访问该列了。

在Percona Server for MySQL中使用VIEWS对数据屏蔽是一个很酷的功能。这样,视图的用户就不会看到secret列的一部分,例如电话号码的后四位数字或某些身份识别号码。

更多详情,请参阅:(Data Masking With Percona Server for MySQL - An Enterprise Feature at a Community Price)


check约束
MySQL 8.0.16终于提供了可用的check约束。在早期版本中,它们被忽略了。这引起了MySQL DBA的切齿痛恨,许多其他数据库的爱好者也会理直气壮地拿这个问题嘲笑MySQL是玩具数据库。但在过去的几年里,检查约束一直存在,只是没有得到充分利用。

约束有两种类型:一种用于列,一种用于表。在下一个示例中,对列b建立了一个约束,以确保它的值大于1。请注意,该约束名为b_gt_1,而且每个schema只能使用一次该约束名称。强烈建议为约束命名,以便在出现问题时更容易识别。

>CREATE TABLE cc (a INT, b INT CONSTRAINT b_gt_1 CHECK (b > 1));
Query OK, 0 rows affected (0.0189 sec)
>INSERT INTO cc (a,b) VALUES (1,2);
Query OK, 1 row affected (0.0064 sec)
>INSERT INTO cc (a,b) VALUES (2,0);
ERROR: 3819: Check constraint 'b_gt_1' is violated.

在下一个示例中,最后定义的约束是在表上,并对两列进行比较。

>CREATE TABLE cc2 (a INT, b INT CONSTRAINT b_gt_2 CHECK (b > 2), check (a > b));
Query OK, 0 rows affected (0.0216 sec)
>INSERT INTO cc2 VALUES (1,2);
ERROR: 3819: Check constraint 'b_gt_2' is violated.
>INSERT INTO cc2 VALUES (1,3);
ERROR: 3819: Check constraint 'cc2_chk_1' is violated.

可以看到,当违反表约束时,服务器会指定约束名称"cc2_chk_1"。当约束失败并弹出错误信息时,你不得不寻找未命名的约束,在这种情况下,只能找到第一个。因此,查找多个未命名约束会很麻烦。

列约束和表约束可以组合使用,只要你愿意让它们变得复杂。

 

Triggers
使用触发器可以解决很多问题。触发器可以在删除表中某一行之前、之后或删除时执行,从而将数据保存到另一个表中。为什么这很重要?它允许你围绕数据变化创建事件。也许当客户删除账户时,你想将其电子邮件地址添加到"不批量发送电子邮件"表中,在另一个表中记录其离开的时间戳,并检查他们可能正在处理的任何订单。

下面的示例是客户故障单账户中客户代表变更时的审计跟踪。出于某种原因,管理层希望了解当前客户代表的变更频率。为清晰起见,以下表省略了时间戳列。

第一个表是故障单。

>CREATE TABLE ticket (id int, customer int, rep_current char(10);
Query OK, 0 rows affected (0.0205 sec)

下一个表是这些故障单的更改日志。

>create table ticket_log (id int, customer int, representative char(10));
Query OK, 0 rows affected (0.0156 sec)

现在设置一个触发器,以便将ticket表中的更改存储到ticket_log表中。

> DELIMITER $$
> CREATE TRIGGER ticket_change AFTER UPDATE ON ticket FOR EACH ROW
-> BEGIN
-> INSERT INTO ticket_log VALUES (OLD.id, OLD.customer, NEW.rep_current);
-> END $$
Query OK, 0 rows affected (0.0088 sec)
> DELIMITER ;

> UPDATE ticket SET rep_current='Mary' WHERE id=10;
Query OK, 1 row affected (0.0034 sec)

Rows matched: 1 Changed: 1 Warnings: 0
>  select * from ticket_log;
+----+----------+----------------+
| id | customer | representative |
+----+----------+----------------+
| 10 | 234 | Mary |
+----+----------+----------------+
1 row in set (0.0009 sec)

根据数据库,可以在表上设置多个触发器,指定其执行顺序,并在需要额外数据安全时定义使用触发器的账户。在这个层面上可以实施的逻辑检查包括客户信用限额、最低订单政策和库存再订购数量检查,这些检查可以使业务保持正常运转。

遗憾的是,MongoDB用户只能在Atlas平台上使用触发器。

 

存储过程
PostgreSQL对存储过程的支持非常出色,而MongoDB对存储过程的支持只能算"还将就",MySQL对存储过程的支持则微乎其微。存储过程比临时查询更安全,有时还能减少网络流量,并能将代码集中在数据库服务器上。
我们想要代码聪明还是数据库聪明?

 

结论
如前所述,大多数开源数据库从业者都将业务逻辑保留在代码中。只要所有编码人员都能理解规则并正确执行这些规则,这种方法就是实用合理的。但随着人员的增加,这些知识的传播可能会出现问题。在这种情况下,可能会出现代价高昂的错误、无意中违反法律,以及行被转换成无用的胡言乱语。

将业务逻辑放在数据库中在很多地方都是合理的。如果只向成年人销售产品,就需要检查客户的年龄。促销细节,比如免运费的最低订单金额为50美元,或者订购十件或十件以上可获得15%的折扣,都很容易实现。手头是否有足够的库存可以立即发货,还是需要提供供应商仓库的预计交货日期?这些逻辑不会分散到多个应用程序中,而是作为防火墙、仲裁者和唯一参考保留在数据库中。

在当今世界,数据库的数量如雨后春笋般增长,而唯一增长更快的是使用这些数据库的项目数量,因此,依靠不断扩大的代码库来执行业务规则和逻辑的能力是不切实际的。扪心自问,如果您的业务因邮政运费增加而导致应用程序代码变更,那么搜索庞大的代码库、更新程序、部署新代码(极有可能遗漏某些应用程序代码)或在数据库层面进行一次变更,是否是最好的办法。

现在是时候开始审视你的系统,看看在哪些地方可以使用上述技术,开始将部分业务逻辑转移到数据库中。在某些情况下,让数据库完成工作是显而易见的解决方案,也是业务逻辑的唯一实现方式。

注:本文原文来自percona官方blog,其中观点不做讨论,只是本人的阅读笔记而已!!!