为啥在 Spring Data JDBC 中将实体-值关系实现为反向引用

Posted

技术标签:

【中文标题】为啥在 Spring Data JDBC 中将实体-值关系实现为反向引用【英文标题】:Why is a entity - value relationship implemented as a back reference in Spring Data JDBC为什么在 Spring Data JDBC 中将实体-值关系实现为反向引用 【发布时间】:2021-09-05 10:40:01 【问题描述】:

在 Spring Data JDBC 中,如果一个实体(例如 Customer)有一个值(例如 Address),如示例 here 中的值有一个反向引用列(表 address 中的列 customer)到 db 架构中的实体:

CREATE TABLE "customer" (
  "id"                  BIGSERIAL       NOT NULL,
  "name"                VARCHAR(255)    NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE "address" (
  "customer"            BIGINT,
  "city"                VARCHAR(255)    NOT NULL
);

这样做的问题是,如果您在一个实体或什至在不同实体中多次使用该 Address 值,则必须为每次使用定义一个额外的列。只有实体的主 ID 存储在这些列中,否则无法区分它是哪个实体。在我的实际实现中,Address 值有五个这样的列:

  "order_address"            BIGINT,    -- backreference for orderAddress to customer id
  "service_address"          BIGINT,    -- backreference for serviceAddress to customer id
  "delivery_address"         BIGINT,    -- backreference for deliveryAddress to customer id
  "installation_address"     BIGINT,    -- backreference for installationAddress to provider_change id
  "account_address"          BIGINT,    -- backreference for accountAddress to payment id

我了解它的工作原理,但我不了解这个反向参考实现背后的想法。那么有人可以解释一下这个问题吗?谢谢!

【问题讨论】:

【参考方案1】:

对于大多数好的问题,答案有很多方面。

历史/对称答案

当涉及到实体之间的引用时,Spring Data JDBC 支持 1:1(您询问的那个)和 1:N(列表、集合和映射)。 对于后者,除了反向引用之外的任何东西都是奇怪/错误的。 并且使用 1:1 的反向引用变得基本相同,简化了代码,这是一件好事。

DML 流程答案

使用反向引用,插入和删除的过程变得更加容易:首先插入聚合根(在您的示例中为customer),然后是所有引用的实体。如果这些实体有更多实体,它会继续工作。删除的工作方式相反,但同样直接。

依赖答案

聚合中的引用实体只能作为该聚合的一部分存在。从这个意义上说,它们依赖于聚合根。没有那个聚合根就没有内部实体,而聚合根通常也可以在没有内部实体的情况下存在。因此,内部实体带有引用是有道理的。

身份证答案

通过这种设计,内部实体甚至不需要 id。它的身份完全由聚合根的身份给出,如果与同一实体类存在多个一对一关系,则使用反向引用列。

替代方案

所有原因或多或少都基于一个一对一的关系。我当然同意,对于同一个班级的两个这样的关系看起来有点奇怪,而在你的例子中是 5,这变得很荒谬。在这种情况下,您可能需要寻找替代方案:

使用地图

不要像这样对Customer 类建模:

class Customer 
  @Id
  Long id;
  String name;
  Address orderAddress
  Address serviceAddress
  Address deliveryAddress
  Address installationAddress
  Address accountAddress

使用这样的地图

class Customer 
  @Id
  Long id;
  String name;
  Map<String,Address> addresses

这会导致像这样的address

CREATE TABLE "address" (
  "customer"            BIGINT,
  "customer_key"        VARCHAR(20).    NOT NULL,
  "city"                VARCHAR(255)    NOT NULL
);

您可以使用@MappedCollection 注释来控制列名,并且您可以根据需要为各个地址添加临时 getter 和 setter。

让它成为真正的价值

您将Address 称为,而我将其称为实体。如果应该将其视为一个值,我认为您应该将其映射为 embedded 像这样

class Customer 
  @Id
  Long id;
  String name;
  @Embedded(onEmpty = USE_NULL, prefix="order_")
  Address orderAddress
  @Embedded(onEmpty = USE_NULL, prefix="service_")
  Address serviceAddress
  @Embedded(onEmpty = USE_NULL, prefix="delivery_")
  Address deliveryAddress
  @Embedded(onEmpty = USE_NULL, prefix="installation_")
  Address installationAddress
  @Embedded(onEmpty = USE_NULL, prefix="account_")
  Address accountAddress

这将使address 表变得多余,因为数据将被折叠到customer 表中:

CREATE TABLE "customer" (
  "id"                  BIGSERIAL       NOT NULL,
  "name"                VARCHAR(255)    NOT NULL,
  "order_city"          VARCHAR(255)    NOT NULL,
  "service_city"        VARCHAR(255)    NOT NULL,
  "deliver_city"        VARCHAR(255)    NOT NULL,
  "installation_city"   VARCHAR(255)    NOT NULL,
  "account_city"        VARCHAR(255)    NOT NULL,
  PRIMARY KEY (id)
);

还是聚合?

但也许您需要自己的地址,而不是作为客户的一部分。 如果是这种情况,地址就是它自己的聚合。 聚合之间的引用应该建模为 id 或 AggregateReference。这在Spring Data JDBC, References, and Aggregates中有更详细的描述

【讨论】:

感谢您的详细说明 - 有一些背景信息总是很高兴!实际上,我首先使用的是 Map 地址。但是我最终在一个实体中使用了三个 Address 值,而另外两个实体使用了每个 Address 值。我不明白...为什么您认为在我的示例中 AddressDDD 上下文中不是真正的 VALUE,而是一个ENTITY?它没有自己的身份。 customer 表中的城市过多。 关于实体与价值。这有点取决于你在哪里看。在数据库中,它确实有一个 id。

以上是关于为啥在 Spring Data JDBC 中将实体-值关系实现为反向引用的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用 Spring Data JPA 更新实体时@Transactional 隔离级别不起作用?

Spring data JDBC没有创建表

Spring Data JDBC 自定义查询参数转换器

如何在 Spring Data JDBC 中插入默认值

spring-data-jdbc 中是不是有相当于@BatchSize

Spring Data JPA JDBC 语句 MADNESS [重复]