我在一个项目中使用 Prisma 和 Postgresql 来存储和管理食谱。
我正在努力实现一种多对多关系,在这种关系中我可以限制一方至少需要一个关系字段值。
食谱可以有一种或多种香气。香气可以包含在零个或多个食谱中。
我使用显式关系表,因为我需要在其中存储附加数据(配方中每种香气的数量)。
配方、香气和关系模型如下:
model Recipe {
id Int @id @default(autoincrement())
name String
base String? @default("50\/50")
description String? @default("aucune description")
rating Int? @default(1)
aromas RecipeToAromas[]
}
model Aroma {
id Int @id @default(autoincrement())
name String
brand Brand? @relation(fields: [brandId], references: [id])
brandId Int? @default(1)
recipes RecipeToAromas[]
@@unique([name, brandId], name: "aromaIdentifier")
}
model RecipeToAromas {
id Int @id @default(autoincrement())
recipeId Int
aromaId Int
quantityMl Int
recipe Recipe @relation(fields: [recipeId], references: [id])
aroma Aroma @relation(fields: [aromaId], references: [id])
}
我想限制食谱至少有一种香气。
根据定义,多对多定义了零到多的关系。
我考虑通过在 Recipe 和 Aroma 之间添加额外的一对多关系来解决问题。
这意味着在配方中添加一个额外的香气字段来存储所需的一种香气(并将香气字段重命名为additionalAromas以避免混淆):
model Recipe {
id Int @id @default(autoincrement())
name String
base String? @default("50\/50")
description String? @default("aucune description")
rating Int? @default(1)
aromas RecipeToAromas[]
aroma Aroma @relation(fields: [aromaId], references: [id])
aromaId Int
}
并在 Aroma 中添加一个配方字段,以建立关系:
model Aroma {
id Int @id @default(autoincrement())
name String
brand Brand? @relation(fields: [brandId], references: [id])
brandId Int? @default(1)
recipes RecipeToAromas[]
recipe Recipe[]
@@unique([name, brandId], name: "aromaIdentifier")
}
但这感觉不对,因为我会有重复的内容:Aroma 中的菜谱和菜谱字段会存储相同的数据。
** 编辑 ** 我尝试使用此解决方案来解决问题,但它产生了第二个问题: 配方中的每种香气在该配方中必须是唯一的(这由关系数据库中的化合物@unique 反映)。
如果我在配方和香气之间添加一对多关系,那么香气可以在配方中存储多次:
await prisma.recipe.create({
data: {
name: "First recipe",
aromaId: 1,
aromas: {
create: [
{ aromaId: 1, quantityMl: 2 },
{ aromaId: 2, quantityMl: 2 },
{ aromaId: 3, quantityMl: 2 },
],
},
},
});
我当然可以通过仅依靠突变函数和用户输入的验证来解决这个问题。当我使用打字稿时,可能会尝试添加类型的安全层。 但我觉得这会使数据库变得脆弱并且容易出错,特别是如果我必须与其他开发人员协作,甚至在不同的项目中使用数据库。
我找不到任何涵盖类似情况的资源,当然我花了很多时间搜索和重新阅读文档。
我是 prisma 的新手(昨天开始),并且对 RDBMS 没有太多经验,所以感觉好像我错过了一些东西。
所以你的问题可以通过两种方式解决(使用原始 SQL),一种比另一种更好。另外,我将使用 PostgreSQL 语法,因为我不熟悉 prisma,但我确信这最多可以转换为 ORM 模型,最坏的情况是作为原始 SQL 语句插入 postgres
使用触发器
触发器与特定表相关联,并且将在该表中完成特定操作时运行,对于我们的情况,我们希望在
Recipe
获取新元素后运行它。语法如下(也请随意阅读文档here,因为这是一个相当复杂的主题,无法用几句话概括)
CREATE OR REPLACE FUNCTION consistency_check_recipe() RETURNS TRIGGER AS $$
BEGIN
IF NOT EXISTS (SELECT * FROM RecipeToAromas WHERE recipeId = NEW.ID) THEN
DELETE FROM Recipe WHERE Recipe.id = NEW.id;
RAISE EXCEPTION 'Must have at least 1 Aroma';
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER recipe_aroma_check
AFTER INSERT OR UPDATE ON Recipe
INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION consistency_check_recipe();
总结一下上面的函数/触发器的作用,一旦创建了新的
Recipe
条目,触发器就会等到事务的最后一刻,然后检查是否存在带有 RecipeToAromas
的 Recipe.id
条目。这也意味着,如果您希望创建新的食谱,您还必须在同一交易中添加香气,例如这会起作用
BEGIN;
INSERT INTO Recipe
VALUES (1, 'Borsh', 'Tasty Water', 'Food Nothing more', 100);
INSERT INTO RecipeToAromas
VALUES (DEFAULT, 1, 3, 4);
COMMIT;
但这不会
INSERT INTO Recipe
VALUES (1, 'Borsh', 'Tasty Water', 'Food Nothing more', 100);
INSERT INTO RecipeToAromas
VALUES (DEFAULT, 1, 3, 4);
由于每个语句都被视为单独的事务,因此触发器将在
Recipe
获取 INSERT 之后但在 RecipeToAromas
获取 INSERT 之前被调用。
使用检查约束
您可以但不应该在检查约束中使用子查询。