具有多个多对多关系的 SQLAlchemy

Posted

技术标签:

【中文标题】具有多个多对多关系的 SQLAlchemy【英文标题】:SQLAlchemy with multiple Many to Many Relationships 【发布时间】:2018-03-28 18:01:50 【问题描述】:

我想用 SQLAlchemy 为食谱创建一个数据库,但是我不确定我的方法是否正确。如何将数据插入 recipe_ingredient 表?

我的做法:

一个食谱有一个名字,可以有多种成分。一种成分由数量、单位和名称(成分表)组成,例如 500 毫升水。

餐桌食谱 - id,主键 - 姓名 (一个配方可以有多种成分,一种成分可以在多个配方中)

表配方_配料

外键(recipe.id) 外键(amounts.id) 外键(units.id) 外键(ingredients.id)

餐桌金额

id,主键 金额(例如 500)

表格单位

id,主键 单位(例如毫升)

餐桌配料

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】:

所以,您的关系定义和您的多对多 recipe_ingredient 表很好。你可以用你拥有的代码做你想做的事。您有一些文体问题使您的代码比应有的更难阅读。

工作原理

我们先来看看你拥有的功能:

Recipe 对象将具有ingredient 属性,其作用类似于列表。您可以将Ingredients 对象附加到它,当您调用它时,您将拥有Ingredients 的常规Python 列表:

# 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 而不是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,哪个听起来好一点

【讨论】:

非常感谢您提供的详细帮助。 Association 对象正是我所需要的。也感谢命名提示,我一定会坚持使用它们;)有一个自己的表有用吗? (我这样做是为了向用户展示已经可用的成分和单位)。但我不确定如何从配料中获取名称:pizza.ingredients[0].name 我已将模型和对象添加到上面的问题中。 amount 作为 int 列是有意义的,但 unit 作为 varchar 没有意义,尤其是在没有任何检查的情况下。例如。对于盎司:ozoZOzOZ 都是可能的 - 显然三个是错误的,但它们是可能的 - 并且架构应该阻止这种情况发生。

以上是关于具有多个多对多关系的 SQLAlchemy的主要内容,如果未能解决你的问题,请参考以下文章

具有多个多对多关系的 SQLAlchemy

在关系数据库中对相同实体之间的多个多对多关系进行建模

Laravel:具有多对多关系的历史排名系统

具有一对多和多对多关系的 JOOQ pojos

从具有多对多关系核心数据iOS的上下文中删除nsmanagedboject

DynamoDB - 关联表 - 多对多关系