实施领域驱动设计:为什么要在所有存储库查询中包含 TenantId?
Implementing Domain Driven Design: Why include TenantId in all repository queries?
我试图理解为什么 Vaughn Vernon(在红皮书的 Github 示例代码中)在每个存储库 get 或 find 方法中包含 tenantId。特别是那些正在做基本 getById 的人。
在一个示例中,他使用用户名来提供用户身份,以及用户名在租户内必须唯一的业务规则,因此:
class UserRepository:
public User userWithUsername(TenantId aTenantId, String aUsername)
很有道理。
但在他的其他 BC 中,他使用的是基于 GUID 的标识值对象,但在从存储库检索时仍将其与 tenantId 结合:
class ProductRepository:
public Product productOfId(TenantId aTenantId, ProductId aProductId)
即使是 CQRS 示例,也将 tenantId 和 entityId 的组合用于其存储库单一 get 方法。
它无处不在,但我不明白为什么有必要。如果产品、积压项目、论坛、日历等都有全球唯一标识符,为什么还需要更多的东西来查询它们?
如果使用它来确保只能检索特定租户的实体 - 这似乎不是存储库的预期责任,而是身份验证等,那么为什么要包含在每个存储库查询方法中?
请注意,我理解为什么需要将 tenantId 作为实体的属性,以及如何使用它来强制执行某些唯一约束。但是,在检索产品时,productOfId(ProductId aProductId) 不够吗?
他确实谈到了这一点,“在多租户环境中,TenantId 实例也被视为唯一身份的一部分。”结合两个 GUID 来确定身份与仅一个 GUID 相比有什么价值?
我认为这不是 DDD 特有的,而是多租户特有的。无论您如何设计应用程序,您都需要能够分辨记录或实体是属于租户还是其他租户。因此,即使 ID 是全局唯一的,因此不需要另一个 ID 部分来进行重复数据删除,您仍然需要租户 ID。
从更实际的角度来看,如果您需要在该数据之上提供 restful API,则可以在每个实体上找到租户 ID 的实用程序。您的 API 结构很可能如下所示:
api/{tenantId}/entity-type/{entityId}
您必须验证执行请求的用户是否可以访问给定的 'tenantId',例如,基于身份验证令牌中的声明。如果用户有访问权限,那么您将从数据库中读取。但是,如果您的存储库只接受 'entityId',那么它将 return 该实体,而不管它属于哪个租户,来自 tenant1 的用户可以从任何其他只知道 ID 的租户那里获取数据。当然,您可以在加载实体后添加对租户 ID 的检查,但迟早您会忘记添加它。相反,如果您遵循始终将 'tenantId' 添加到存储库的做法,那么此检查将内置到查询本身中,从而使整个过程更加一致和高效。
附带说明一下,还有其他方法可以对所有查询添加租户 ID 检查,这将实现相同的目标,而无需手动将其传递给每个存储库方法调用和查询实现。例如,您可以将 tenantId 放在上下文中并使用 DI 解析您的存储库来注入它。使用 SQL 服务器,您可以使用行级安全性,以便 tenantId 检查成为 table 策略的一部分,而不是将其添加到 table 上的所有查询中。
我试图理解为什么 Vaughn Vernon(在红皮书的 Github 示例代码中)在每个存储库 get 或 find 方法中包含 tenantId。特别是那些正在做基本 getById 的人。
在一个示例中,他使用用户名来提供用户身份,以及用户名在租户内必须唯一的业务规则,因此:
class UserRepository:
public User userWithUsername(TenantId aTenantId, String aUsername)
很有道理。
但在他的其他 BC 中,他使用的是基于 GUID 的标识值对象,但在从存储库检索时仍将其与 tenantId 结合:
class ProductRepository:
public Product productOfId(TenantId aTenantId, ProductId aProductId)
即使是 CQRS 示例,也将 tenantId 和 entityId 的组合用于其存储库单一 get 方法。
它无处不在,但我不明白为什么有必要。如果产品、积压项目、论坛、日历等都有全球唯一标识符,为什么还需要更多的东西来查询它们?
如果使用它来确保只能检索特定租户的实体 - 这似乎不是存储库的预期责任,而是身份验证等,那么为什么要包含在每个存储库查询方法中?
请注意,我理解为什么需要将 tenantId 作为实体的属性,以及如何使用它来强制执行某些唯一约束。但是,在检索产品时,productOfId(ProductId aProductId) 不够吗?
他确实谈到了这一点,“在多租户环境中,TenantId 实例也被视为唯一身份的一部分。”结合两个 GUID 来确定身份与仅一个 GUID 相比有什么价值?
我认为这不是 DDD 特有的,而是多租户特有的。无论您如何设计应用程序,您都需要能够分辨记录或实体是属于租户还是其他租户。因此,即使 ID 是全局唯一的,因此不需要另一个 ID 部分来进行重复数据删除,您仍然需要租户 ID。
从更实际的角度来看,如果您需要在该数据之上提供 restful API,则可以在每个实体上找到租户 ID 的实用程序。您的 API 结构很可能如下所示:
api/{tenantId}/entity-type/{entityId}
您必须验证执行请求的用户是否可以访问给定的 'tenantId',例如,基于身份验证令牌中的声明。如果用户有访问权限,那么您将从数据库中读取。但是,如果您的存储库只接受 'entityId',那么它将 return 该实体,而不管它属于哪个租户,来自 tenant1 的用户可以从任何其他只知道 ID 的租户那里获取数据。当然,您可以在加载实体后添加对租户 ID 的检查,但迟早您会忘记添加它。相反,如果您遵循始终将 'tenantId' 添加到存储库的做法,那么此检查将内置到查询本身中,从而使整个过程更加一致和高效。
附带说明一下,还有其他方法可以对所有查询添加租户 ID 检查,这将实现相同的目标,而无需手动将其传递给每个存储库方法调用和查询实现。例如,您可以将 tenantId 放在上下文中并使用 DI 解析您的存储库来注入它。使用 SQL 服务器,您可以使用行级安全性,以便 tenantId 检查成为 table 策略的一部分,而不是将其添加到 table 上的所有查询中。