SQLAlchemy多对多关系的实现方式

3
我想使用SQLAlchemy创建一个食谱数据库,但我不确定我的方法是否正确。如何向recipe_ingredient表插入数据?
我的方法:
一个食谱有一个名称,并且可以有多个成分。一个成分包括数量、单位和名称(成分表),例如500ml水。
表格recipe - id,主键 - 名称 (一个食谱可以有多个成分,一个成分可以在多个食谱中)
表格recipe_ingredient
- 外键(recipe.id) - 外键(amounts.id) - 外键(units.id) - 外键(ingredients.id)
表格amounts
- id,主键 - 数量(例如500)
表格units
- id,主键 - 单位(例如ml)
表格ingredients
- id,主键 - 成分(例如水)
代码:
recipe_ingredient = db.Table('recipe_ingredient',
                        db.Column('idrecipe', db.Integer, db.ForeignKey('recipe.id')),
                        db.Column('idingredient', db.Integer, db.ForeignKey('ingredients.id')),
                        db.Column('idunit', db.Integer, db.ForeignKey('units.id')),
                        db.Column('idamount', db.Integer, db.ForeignKey('amounts.id'))
)

class Recipe(db.Model):
    id= db.Column(db.Integer, primary_key=True, autoincrement=True)
    name= db.Column(db.VARCHAR(70), index=True)
    ingredient= db.relationship("Ingredients", secondary=recipe_ingredient, backref='recipe')
    amounts = db.relationship("Amounts", secondary=recipe_ingredient, backref='recipe')
    units= db.relationship("Units", secondary=recipe_ingredient , backref='recipe')

class Ingredients(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    ingredient = db.Column(db.VARCHAR(200))

class Units(db.Model):
    id= db.Column(db.Integer, primary_key=True, autoincrement=True)
    unit= db.Column(db.VARCHAR(45), nullable=False)

class Amounts(db.Model):
    id= db.Column(db.Integer, primary_key=True, autoincrement=True)
    amount= db.Column(db.VARCHAR(45), nullable=False)


更新:

class RecipeIngredient(db.Model):
    __tablename__ = 'recipe_ingredient'
    recipe_id = db.Column(db.Integer, db.ForeignKey('recipe.id'), primary_key=True)
    ingredient_id = db.Column(db.Integer, db.ForeignKey('ingredient.id'), primary_key=True)
    amount = db.Column(db.Integer, db.ForeignKey('amount.id'))
    unit = db.Column(db.Integer, db.ForeignKey('unit.id'))
    recipes = relationship("Recipe", back_populates="ingredients")
    ingredients = relationship("Ingredient", back_populates="recipes")


class Recipe(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.VARCHAR(70), index=True)
    ingredients = relationship("RecipeIngredient", back_populates="recipes")


class Ingredient(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.VARCHAR(200))
    recipes = relationship("RecipeIngredient", back_populates="ingredients")


class Unit(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.VARCHAR(45), nullable=False)


class Amount(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    number = db.Column(db.VARCHAR(45), nullable=False)

我添加了以下对象。
pizza = Recipe(name='Pizza')
flour = Ingredient(name='Flour')
water = Ingredient(name='Water')
g = Unit(name='g')
ml = Unit(name='ml')
a500 = Amount(number='500')
a100 = Amount(number='100')

r1= RecipeIngredient(recipe_id=pizza.id, ingredient_id=flour.id, amount=a500.id, unit=g.id)
r2= RecipeIngredient(recipe_id=pizza.id, ingredient_id=water.id, amount=a100.id, unit=ml.id)

pizza.ingredients的结果:

[<RecipeIngredient 1, 1>, <RecipeIngredient 1, 3>]

如果我想获取一个配料的名称,需要在这个模型中添加什么内容呢?使用下面的代码:pizza.ingredients[0].name

1个回答

7
所以,你的关系定义和你的多对多recipe_ingredient表是正确的。你可以使用现有代码做你想做的事。但你的一些风格问题使得代码比应该更难读懂。

工作原理

让我们首先来看看你拥有的功能: Recipe对象将具有像列表一样的ingredient属性。你可以向它附加Ingredients对象,并在调用时你将拥有一个普通的Python列表 Ingredients :
# Make a cake, 
cake = Recipe(name='Cake')
flour = Ingredients(ingredient='Flour')
eggs = Ingredients(ingredient='Eggs')
cake.ingredient.append(flour)
cake.ingredient.append(eggs)
for i in cake.ingredient:
    print(i.ingredient)

因为您已经定义了Recipe.amountsecondary关系,其中secondary=recipe_ingredient,所以SQLAlchemy拥有管理多对多关系所需的所有信息。

Ingredients对象将具有类似于列表的recipe属性,并引用相同的关系:

# Find recipes using flour
for r in flour.recipe:
    print(r.name)

你可以在食材中添加菜谱,而不是将食材添加到菜谱中,这样同样有效。
# Make cookies
cookies = Recipe(name='Cookies')
eggs.recipe.append(cookies)
for i in cookies.ingredient:
    print(i.ingredient)

阅读方式

您可能会注意到,您命名事物的方式使它读起来有点拙劣。当任何属性引用一对多关系时,使用复数形式会更清晰。例如,在Recipe中的ingredients关系如果实际上称为ingredients而不是ingredient,则读起来会好很多。这将允许我们通过cake.ingredients遍历。同样适用于反向方向:将backref称为recipes而不是recipe,将使flour.recipes更清晰地指多个链接的配方,而flour.recipe可能会有点误导。

还存在关于对象是复数还是单数的不一致性。 Recipe是单数,但Ingredients是复数。老实说,关于哪种是正确的样式的意见并不普遍-我喜欢在我的所有模型中使用单数RecipeIngredientAmountUnit-但那只是我个人的偏好。选择一个样式并坚持使用它,而不是在它们之间切换。

最后,属性名称有点冗余。 Ingredients.ingredient有点过头了。 Ingredient.name更有意义,也更清晰。

还有一件事

这里有一个额外的事情-在我看来,您想存储有关食谱/配料关系的其他信息,即成分的数量和单位。您可能需要2个鸡蛋或500克的面粉,我猜这就是您的AmountsUnits表的用途。在这种情况下,这些额外的信息是您关系的重要组成部分,而不是尝试在单独的表中匹配它们,则可以直接在您的关联表中使用它们。这需要一些工作- SQLAlchemy文档更深入地介绍了如何使用Association object来管理此附加数据。简而言之,使用此模式将使您的代码更清洁,更易于长期管理。

更新

@user7055220,在我的看法中,您不需要单独的AmountUnit表,因为这些值只有作为RecipeIngredient关系的一部分才有意义。我将删除那些表,并将RecipeIngredient中的amountunit属性更改为直接存储单位/数量的StringInteger值:

class RecipeIngredient(db.Model):
    __tablename__ = 'recipe_ingredient'
    recipe_id = db.Column(db.Integer, db.ForeignKey('recipe.id'), primary_key=True)
    ingredient_id = db.Column(db.Integer, db.ForeignKey('ingredient.id'), primary_key=True)
    amount = db.Column(db.Integer)
    unit = db.Column(db.VARCHAR(45), nullable=False)
    recipes = relationship("Recipe", back_populates="ingredients")
    ingredients = relationship("Ingredient", back_populates="recipes")

无论哪种情况,回答您如何访问成分值的问题 - 在您的示例中,pizza.ingredients 现在包含一个关联对象数组。成分是该关联对象的子级,并且可以通过其 ingredients 属性访问。如果按照我上面提出的更改进行更改,则可以直接访问 unitamount 值。访问这些数据将如下所示:
for i in pizza.ingredients:
    print(i.amount) # This only works if you're storing the amount directly in the association proxy table as per my example above
    print(i.unit) # Same goes for this
    print(i.ingredients.name) # here, the ingredient is accessed via the "ingredients" attribute of the association object

或者,如果您只想使用您所问的语法:
print(pizza.ingredients[0].ingredients.name)

关于命名还有一点需要注意:请注意在你的关联对象中,backref对象被称为ingredients,即使它只映射到单个配料,所以应该是单数形式——那么上面的例子就会变成pizza.ingredients[0].ingredient.name,听起来更好一些。


非常感谢您提供的详细帮助。关联对象正是我所需要的。 同时也感谢您提供的命名技巧,我一定会遵循它们的;) 对于数量是否有自己的表格是否有用?(我为成分和单位做了这个,以向用户展示哪些已经可用)。但我不确定如何通过 pizza.ingredients[0].name 获取成分的名称。 我已经将我的模型和对象添加到了上面的问题中。 - user7055220
amount 作为 int 列是有意义的,但 unit 作为 varchar 不是很合适,特别是没有任何检查。例如,对于盎司:ozoZOzOZ 都是可能的 - 显然有三个是错误的,但它们是可能的 - 因此架构应该防止这种情况发生。 - kevlarr

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