处理状态困境

20

关于状态字段和类似的预定义值集合,存在一个经常性的问题。

我们以订单系统为例,订单实体有一个状态字段,可能是“新建”、“进行中”、“已支付”等。

问题:

订单状态需要:

  • 存储(在数据库中)
  • 处理(在后端处理)
  • 传达(通过Web服务API向前端通信)

如何在保留以下条件的情况下执行这三个活动:

  • 保留状态的含义。
  • 高效的存储。

以下是一些带有其优缺点的示例实现:

1- 状态表

  • 数据库将包含一个id、名称的状态表
  • 订单表引用状态的id。

  • CREATE TABLE `status` (
      `id` INT NOT NULL,
      `name` VARCHAR(45) NOT NULL,
      PRIMARY KEY (`id`));
    
    CREATE TABLE IF NOT EXISTS `order` (
      `id` INT NOT NULL AUTOINCREMENT,
      `status_id` INT NOT NULL,
      PRIMARY KEY (`id`),
      INDEX `order_status_idx` (`status` ASC),
      CONSTRAINT `order_status_id`
        FOREIGN KEY (`status_id`)
        REFERENCES `status` (`id`)
        ON DELETE NO ACTION
        ON UPDATE NO ACTION);
    
    后端代码有一个枚举,为这些预定义的整数在代码中赋予了意义。
    enum Status {
        PAID = 7;
    };
    
    // While processing as action ...
    order.status = Status::PAID;
    
    • 网络服务 API 将返回状态码

    order: { id: 1, status_id: 7 }
    
  • 前端代码有一个类似的枚举,给这些预定义的整数在代码中赋予了意义。(与后端代码类似)

  • 优点:

    • 数据库定义良好且规范化
  • 缺点:
    • 状态编号和意义之间的映射在三个地方完成,这给人类造成了错误和不一致性,难以定义特定状态编号的含义。
    • API返回的数据不具备描述性,因为status_id:7并没有传递出具体的含义,因为它不包括status_id:7的含义。

2- 状态枚举

  • 在数据库中,订单表将包含一个类型为ENUM的状态列,其中包含预定义的状态。

  • CREATE TABLE IF NOT EXISTS `order` (
      `id` INT NOT NULL AUTOINCREMENT,
      `status` ENUM('PAID') NULL,
      PRIMARY KEY (`id`));
    
  • 后端代码将预定义状态设置为代码工件的常量值

  • enum Status {
        PAID = 'PAID'
    };
    
    class Status {
    public:
        static const string PAID = PAID;
    };
    

    请按以下方式使用

    // While processing as action ...
    order.status = Status::PAID;
    
  • 网络服务 API 将返回状态常量

  • order: { id: 1, status: 'PAID' }
    
  • The frontend code will have a similar construct for predefined status constants, as in the backend code.

  • Pros:

    • The database is well-defined and normalized, with a separate status table that allows for easier modifications without expensive ALTER commands.
    • The returned data from the API is descriptive and delivers the required meaning.
    • The status constants used already contain their meaning, which reduces the chances of errors.
  • Cons:
    • None mentioned.

3- My proposed solution:

  • The database will contain a status table with one field called key with type string, which is the primary key of this table.

  • CREATE TABLE `status` (
      `key` VARCHAR(45) NOT NULL,
      PRIMARY KEY (`key`));
    
  • 订单表将包含名为status的字段,类型为字符串,该字段引用status表的key字段。

  • CREATE TABLE IF NOT EXISTS `order` (
      `id` INT NOT NULL AUTOINCREMENT,
      `status` VARCHAR(45) NOT NULL,
      PRIMARY KEY (`id`),
      INDEX `order_status_idx` (`status` ASC),
      CONSTRAINT `order_status`
        FOREIGN KEY (`status`)
        REFERENCES `status` (`key`)
        ON DELETE NO ACTION
        ON UPDATE NO ACTION);
    
  • 后端代码将预定义状态的常数值作为代码构件

  • enum Status {
        PAID = 'PAID'
    };
    
    class Status {
    public:
        static const string PAID = PAID;
    };
    

    应按以下方式使用

    // While processing as action ...
    order.status = Status::PAID;
    
    网络服务API将返回状态常量。
    order: { id: 1, status: 'PAID' }
    
  • 前端代码将具有类似于预定义状态常量的结构。(与后端代码类似)

  • 优点:

    • 数据库已经定义并规范化
    • 从API返回的数据是描述性的,并提供所需的含义。
    • 使用的状态常量已经包含它们的含义,从而减少了出错的可能性。
    • 通过在状态表中使用INSERT命令添加新的状态常量非常简单。
  • 缺点:
    • ???

我想知道这是否是可行的解决方案,或者是否存在更好的解决方案来解决这个反复出现的问题。

请说明为什么所提出的解决方案不好,并且您的更好解决方案为何更好。

谢谢。


1
使用varchar作为主键(熵)时,可以使用枚举类型。但是,由于您使用逻辑键作为FK,数据库变得不太规范化。在我看来,最好将数据库保持为查找表状态,并且不必将数据库结构公开到API作为其端点。对于前端,您可以遵循任何模式并进行转换以使其成为可接受的数据类型或结构 - 例如MVVM,并依赖ModelView来处理它。 - Abdelrahman M. Allam
请提供一个包括以下内容的回答:1)建议解决方案的不良原因。2)为什么您的更好的解决方案更好。考虑以下因素:1)完整性。2)预防性工程。3)性能。4)所有级别的一致性。5)您认为与该情况相关的任何其他质量属性。 - Meena Alfons
每个可行的解决方案都是一个解决方案,1)所提出的解决方案不符合规范化规则(n1st,n2nd),2)我的解决方案(你在问题中使用的第一个)通过(fk)保持数据库规范化,让RestAPI返回{status:'PAID'}而不是假设API应该公开数据库结构,这只是没有完成的简单操作,在代码中只需返回与关系匹配的字符串值的状态清晰属性对我来说性能将受到影响,因为需要从状态表中加载相关行。 - Abdelrahman M. Allam
1
我认为创建一个单列表并将其用作状态字段的外键没有任何优势。这正是枚举所做的。因此,考虑到2和3是相同的,3更好,因为它更简单。至于其他方面:需要很多约定来传达状态。没关系,只需在文档中写下不同的状态值即可。这是后端API的一部分,前端应该使用它。 - Jelmer Jellema
@JelmerJellema 我们最终可能会认同,没有魔法(自动)方式来处理状态,因此它需要成为后端 API 文档的一部分。但是针对2和3的比较:外键对该字段施加了约束。据我所知,枚举字段在添加记录到状态表时更昂贵,而添加记录则更便宜。 - Meena Alfons
@MeenaAlfons 当然,约束条件有所帮助,而在大表中更改枚举的可能值可能会导致锁定和处理时间方面的麻烦。但是再说一遍:它使表格自包含且更易读。我更关心人们的使用体验,而不是让计算机辛苦工作;-) - Jelmer Jellema
2个回答

1
这是我解决问题的方法:
  1. orders 表中添加一个类型为 stringstatus 列。
  2. 在类中定义所有状态的常量,以便您可以轻松引用它们。
  3. 在创建订单时制定验证规则,确保状态值仅限于您之前定义的允许值。
这样,通过编辑代码库就可以很容易地添加新状态,并且状态的检索值仍然是字符串(具有描述性)。
希望这回答了您的问题。

好的,但是数据库将允许在status中输入任何字符串,并且数据库对该字段可能的值既没有信息也没有约束。你对此有什么看法? - Meena Alfons
2
我觉得这不应该成为大问题,因为你不应该直接与数据库层交互(尤其是创建记录);总应该有一层应用程序逻辑来检索、创建、更新或删除数据库中的数据。 - Ramy Tamer
提议的解决方案已经包含了您的解决方案,并增加了两个优点:1)数据库包括有关可用状态的信息,使得数据库更加完整。2)“状态”字段的域被限制为“状态”的接受值。 - Meena Alfons

0
我建议这样做:
  1. 将状态存储在数据库中,使用 status(unsigned tinyint, char(5)) 格式。
  2. id 必须是 2 的幂:1、2、4、8 等等。
  3. 在后端代码中,常量名称必须是人性化的,但值必须是整数:const PAID = 2
  4. 在后端中,您不应直接使用常量,而应使用状态类对象,该对象将包含一些方法,如 valuename
  5. 此类的测试将检查其所有值是否在数据库中,并且所有数据库的值是否都被类覆盖。

留出空间以防人为错误

测试的目的是避免人为错误。

状态通常不会很复杂,也没有太多值需要处理。

枚举类型是有害的。http://komlenic.com/244/8-reasons-why-mysqls-enum-data-type-is-evil/

关于您的提议:

数据库已经定义良好并且规范化

不是的,它被去规范化了。

API返回的数据具有描述性并传达所需的含义。

您始终可以使用包装器,进入状态表以获取人类名称。

已经使用的状态常量包含它们的含义,从而降低了错误的几率。

常量名称是为人类设计的,而值是为Benders设计的。

通过在状态表中使用INSERT命令,添加新的状态常量非常简单。

在我的解决方案和第一个解决方案中都是相同的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接