使用 ORM 时如何确保域完整性

Posted

技术标签:

【中文标题】使用 ORM 时如何确保域完整性【英文标题】:How to ensure domain integrity when using an ORM 【发布时间】:2019-03-26 13:31:25 【问题描述】:

除了声明列类型之外,应该如何强制域完整性?

ENUM() 是一个选项,但是从 SQL 角度和 Doctrine 角度来看,它都有缺点。

作为替代方案,在使用 ORM 之前设计数据库时,我会改为使用自然键和外部约束。虽然我的time_unit 表有一个名称和秒列,但它唯一真正的意义是将其他表中的值限制为time_unit.unit 的值。

MariaDB [tracker]> select * from time_unit;
+------+-----------+----------+
| unit | name      | seconds  |
+------+-----------+----------+
| d    | Days      |    86400 |
| h    | Hours     |     3600 |
| i    | Minutes   |       60 |
| m    | Month     |  2592000 |
| q    | Quarter   | 10368000 |
| s    | Seconds   |        1 |
| w    | Week      |   604800 |
| y    | Year      | 31536000 |
+------+-----------+----------+
8 rows in set (0.01 sec)

MariaDB [tracker]> select * from sign;
+------+
| sign |
+------+
|   -1 |
|    1 |
+------+
2 rows in set (0.00 sec)

CREATE TABLE `agenda` (
  `id` int(11) NOT NULL,
  `time_value` smallint(6) DEFAULT NULL,
  `time_unit` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `sign` smallint(6) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_agenda_time_unit1_idx` (`time_unit`),
  KEY `fk_agenda_sign1_idx` (`sign`),
  CONSTRAINT `FK_5A9C89CF7106057E` FOREIGN KEY (`time_unit`) REFERENCES `time_unit` (`unit`),
  CONSTRAINT `FK_22ACC67D9F7E91FE` FOREIGN KEY (`sign`) REFERENCES `sign` (`sign`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

但这在使用 ORM 时会出现问题。首先,为每个对象创建一个对象,这似乎过多并使序列化变得痛苦。其次,我read 表示,在使用 ORM 时,不应该使用外键,而是使用关联的对象。

在数据库级别,表示实体之间的关系 通过外键。但是有了 Doctrine,你将永远不必(也永远不会 应该)直接使用外键。你应该只与 通过自己的身份表示外键的对象。

为了强制域完整性,是否应该使用 ENUM()、自然键的外部约束或其他方法?

【问题讨论】:

“域完整性”是什么意思?可能这些规则过于复杂,无法在 SQL 的微薄功能中实现,即使使用触发器、FK、数据类型、约束等。在您的客户端代码中执行。 @RickJames 我指的是确保给定的列值必须在一组指定的可能值中。 FK 通常工作得很好,但是 Doctrine 会尝试为其创建一个不需要的对象。 <snide-remark> 有时,第 3 方软件的障碍多于帮助。 </snide-remark> 【参考方案1】:

这主要是风格问题。在构建模式时,参照完整性是保证质量的一种方式。还有其他方法可以做到这一点 - 例如,单元测试保证系统不会接受除 1 和 -1 之外的“符号”值。

因此,如果您有一个仅用于限制有效条目的表,如果没有其他属性,我建议您做最简单的事情并将该逻辑放在您的应用程序层中,并通过单元测试来验证你想要的行为。只需将数据作为没有外键的整数存储在数据库中。

如果你真的想让数据库验证条目,你可以使用枚举,或者告诉 ORM 该列只是一个整数,但仍然创建外键。这是否是一个好主意又取决于风格。我非常喜欢“DRY”——不要重复自己。如果您有逻辑来限制应用程序层中属性的有效选项,请专注于使其更好,不要在数据库模式中重复该逻辑。不过,这并不是那么简单——如果您认为人们会在您的应用程序之外访问数据库,那么拥有外键或枚举是合理的。

我认为您的time_unit 不仅仅是一组查找值 - 我猜“秒”列用于在单位之间进行转换。这里有几个选项,但我还是想依靠单元测试来验证我的转换逻辑,如果发生转换逻辑,您可能会考虑将这些作为常量存储在应用程序代码中。然后,您可以将该单元作为 char 列存储在议程表中。

这使您的持久性逻辑更容易和更快,但将验证行为的责任置于您的单元测试中,而不是您的模式中。

我认为关于从不直接使用外键的引用旨在表明使用您的 ORM 的“正常”行为是通过请求议程对象 (print agendaItem.time_unit.name) 来访问 time_unit,而不是明确地请求外键键并检索相关对象 (timeUnitID = agendaItem.timeUnitID; print time_unit.findByID(timeUnitID)。我不认为这是针对外键的全面建议。

【讨论】:

感谢 Neville,对于“符号”表,为什么不包含外键约束,以便 DB 只强制执行允许的值,但让 ORM 忽略符号表,所以它不会创建一个Sign 对象? 我已经更新了答案以反映这一点 - 我更喜欢“不要重复自己”,但这是一个风格问题......【参考方案2】:

ENUM() 是一个选项,但是从 SQL 的角度来看它有缺点 以及教义的观点。

为了强制域完整性,应该 ENUM(),对一个域的外部约束 自然键,还是使用其他方法?

您可以替换 mysql 的 / MariaDB 的数据类型

ENUM('1', '2', '3'); 

采用更理性的方法。

    CREATE TABLE enum_data (
       position TINYINT
     , value CHAR(1)
    );

INSERT INTO enum_data (position, value) VALUES(1, '1');
INSERT INTO enum_data (position, value) VALUES(2, '2');
INSERT INTO enum_data (position, value) VALUES(3, '3');

并在你的表中使用它

CREATE TABLE your_table (
   id INT ....
 , enum_data_id TINYINT
 ...
 ...
)

在场外,您也可以/应该也使用FOREIGN KEY 以合理的方法确保数据完整性。 与ENUM 相比,使用额外的表也具有优势。 当您想从ENUM 数据类型中添加、编辑、删除时,您将需要使用ALTER TABLE,这是在使用其他表时不需要的。

【讨论】:

谢谢雷蒙德。这不会导致创建EnumData 对象吗?还有一点题外话,但你会建议在使用 ORM 时只使用代理键吗? @user1032531 我已经很长时间没有使用学说了。我也不能提出建议,如果你应该或不应该使用有很多理论的代理键.. 也我不知道你的数据模型,如果你有不改变使用的自然键 再次感谢。明白了。只是好奇,但是您使用的是另一个 ORM 吗?我一直只是简单地编写我的 SQL 并使用 php 执行,而整个 Doctrine 事情花费的时间比我预期的要多。 " 只是好奇,但你在使用另一个 ORM 吗?" 我已经构建并使用了我自己的抽象数据库层 @user1032531,它支持对象关系映射、Active Record、使用跨数据库供应商的方法链接进行直接查询和查询构建。它还支持优化事务中的查询,例如多个更新查询将被重写为一个更新查询,其中包含一个 case 语句。

以上是关于使用 ORM 时如何确保域完整性的主要内容,如果未能解决你的问题,请参考以下文章

数据库知识

nginx 将数据不完整地传输到 unix 域套接字

外键约束的作用

数据完整性约束

SQL-Base 用表组织数据

SQL-Base 用表组织数据