Flask SQLAlchemy:如何添加依赖于另一个 table 的列?
Flask SQLAlchemy: How to add a column that depends on another table?
我的 SQLAlchemy 数据库(使用 Flask SQLAlchemy)中有三个 table:产品、产品变体和订单。我想查看订单中包含的产品及其变体。
它与 relationships/foreign 键配合使用效果很好,但主要问题是:如果我在订单中添加一个产品,我仍然可以添加另一个产品的变体(使用 Flask-Admin,或仅使用 flask shell).
那么,主要问题: 如何在 table 之间建立联系,以便仅当它们是订单产品的变体时才能添加变体?谢谢:)
另一个解决方案:如何将列添加到订单 table,以便根据变体 ID 从产品 table 获取产品名称?我尝试使用 column_property
、Post.query.get(variation_id
)、variation.parent_id
、backhref variation.origin_product
但没有任何成功:)
我的模特:
产品(如三星 Galaxy 7)
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), index=True)
brand = db.Column(db.String(120))
variations = db.relationship('Variation', backref='origin_product', lazy='dynamic')
orders = db.relationship('Order', backref='product_in_order', lazy='dynamic')
产品变体(如 Samsung Galaxy 7 Blue 32GB)
class Variation(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), index=True)
price = db.Column(db.Integer)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
orders = db.relationship('Order', backref='variation_in_order', lazy='dynamic')
订单
class Order(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
variation_id = db.Column(db.Integer, db.ForeignKey('variation.id'))
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
P.S. product_id = db.Column(db.Integer, db.ForeignKey('variation.product_id'))
在数据库中工作,我看到正确的产品 ID。 Flask-Admin 等外部工具仍然将 product_id
列视为变体对象,因此没有用。需要一种方法,将产品对象连接到 product_id
。例如,连接产品 ForeignKey
,但基于 variation_id
。
防止不相关的产品和变体组合的一种方法是创建从订单到产品的外键和从订单到变体的 overlapping composite foreign key。为了能够引用 variation.id, variation.product_id
的组合,产品 ID 也应该成为主键的一部分,并且必须显式地为 ID 提供自动递增行为:
class Variation(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'),
primary_key=True)
class Order(db.Model):
product_id = db.Column(db.Integer, nullable=False)
variation_id = db.Column(db.Integer)
__table_args__ = (
db.ForeignKeyConstraint([product_id], ['product.id']),
db.ForeignKeyConstraint([product_id, variation_id],
['variation.product_id', 'variation.id']),
)
由于外键默认为 MATCH SIMPLE,变体的复合外键将允许添加变体 ID 为 NULL 的行,但如果给定变体 ID,则组合必须引用现有行。此设置允许将现有关系 product_in_order 和 variation_in_order 用于 Product
和 Variation
分别代替下面涉及更多的模型,尽管 SQLAlchemy 会(正确地)警告这些关系存在冲突,因为它们都设置了产品 ID。创建订单时只需使用其中之一:
In [24]: o1 = Order(product_in_order=product)
In [25]: o2 = Order(variation_in_order=variation)
或跟随documentation about resolving the conflict。在此模型中,产品名称始终可用
In [31]: o1.product_in_order.name
另一种防止在给定产品时向订单添加不相关变体的选项是在这种情况下完全避免添加变体,反之亦然:
class Order(db.Model):
...
variation_id = db.Column(db.Integer, db.ForeignKey('variation.id'))
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
__table_args__ = (
# Require either a variation or a product
db.CheckConstraint(
'(variation_id IS NOT NULL AND product_id IS NULL) OR '
'(variation_id IS NULL AND product_id IS NOT NULL)'),
)
建立与 Product
的关系在此模型中有点复杂,需要 using a non primary mapper:
product_variation = db.outerjoin(
Product, db.select([Variation.id,
Variation.product_id]).alias('variation'))
ProductVariation = db.mapper(
Product, product_variation, non_primary=True,
properties={
'id': [product_variation.c.product_id,
product_variation.c.variation_product_id],
'variation_id': product_variation.c.variation_id
})
连接生成的可选对象映射回 Product
,但也允许基于 Variation.id
选择:
Order.product = db.relationship(
ProductVariation,
primaryjoin=db.or_(Order.product_id == ProductVariation.c.id,
Order.variation_id == ProductVariation.c.variation_id))
这样您就可以使用
从 Order
实例访问产品名称
order.product.name
演示:
In [2]: p1 = Product(name='Product 1')
In [3]: v11 = Variation(product=p1)
In [4]: v12 = Variation(product=p1)
In [5]: p2 = Product(name='Product 2')
In [6]: v21 = Variation(product=p2)
In [9]: session.add_all([p1, p2])
In [10]: session.add_all([v11, v12, v21])
In [11]: session.commit()
In [12]: o1 = Order(product_id=p1.id)
In [13]: o2 = Order(variation_id=v12.id)
In [14]: o3 = Order(variation_id=v11.id)
In [15]: o4 = Order(product_id=p2.id)
In [16]: o5 = Order(variation_id=v21.id)
In [17]: session.add_all([o1, o2, o3, o4, o5])
In [18]: session.commit()
In [25]: [o.product.name for o in session.query(Order).all()]
Out[25]: ['Product 1', 'Product 1', 'Product 1', 'Product 2', 'Product 2']
LEFT JOIN 确保没有变化的产品也能正常工作:
In [26]: p3 = Product(name='Product 3')
In [27]: session.add(p3)
In [28]: session.commit()
In [29]: session.add(Order(product_id=p3.id))
In [30]: session.commit()
In [31]: [o.product.name for o in session.query(Order).all()]
Out[31]: ['Product 1', 'Product 1', 'Product 1', 'Product 2', 'Product 2', 'Product 3']
另一方面,您可以使用所描述的 CheckConstraint
和普通的 property
:
而不是这种相当复杂的结构
class Order(db.Model):
...
@property
def product(self):
if self.product_in_order:
return self.product_in_order
else:
return self.variation_in_order.origin_product
请注意,如果没有预先加载,这将在变化顺序的情况下针对数据库触发 2 个单独的 SELECT 查询。
我的 SQLAlchemy 数据库(使用 Flask SQLAlchemy)中有三个 table:产品、产品变体和订单。我想查看订单中包含的产品及其变体。
它与 relationships/foreign 键配合使用效果很好,但主要问题是:如果我在订单中添加一个产品,我仍然可以添加另一个产品的变体(使用 Flask-Admin,或仅使用 flask shell).
那么,主要问题: 如何在 table 之间建立联系,以便仅当它们是订单产品的变体时才能添加变体?谢谢:)
另一个解决方案:如何将列添加到订单 table,以便根据变体 ID 从产品 table 获取产品名称?我尝试使用 column_property
、Post.query.get(variation_id
)、variation.parent_id
、backhref variation.origin_product
但没有任何成功:)
我的模特:
产品(如三星 Galaxy 7)
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), index=True)
brand = db.Column(db.String(120))
variations = db.relationship('Variation', backref='origin_product', lazy='dynamic')
orders = db.relationship('Order', backref='product_in_order', lazy='dynamic')
产品变体(如 Samsung Galaxy 7 Blue 32GB)
class Variation(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), index=True)
price = db.Column(db.Integer)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
orders = db.relationship('Order', backref='variation_in_order', lazy='dynamic')
订单
class Order(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
variation_id = db.Column(db.Integer, db.ForeignKey('variation.id'))
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
P.S. product_id = db.Column(db.Integer, db.ForeignKey('variation.product_id'))
在数据库中工作,我看到正确的产品 ID。 Flask-Admin 等外部工具仍然将 product_id
列视为变体对象,因此没有用。需要一种方法,将产品对象连接到 product_id
。例如,连接产品 ForeignKey
,但基于 variation_id
。
防止不相关的产品和变体组合的一种方法是创建从订单到产品的外键和从订单到变体的 overlapping composite foreign key。为了能够引用 variation.id, variation.product_id
的组合,产品 ID 也应该成为主键的一部分,并且必须显式地为 ID 提供自动递增行为:
class Variation(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'),
primary_key=True)
class Order(db.Model):
product_id = db.Column(db.Integer, nullable=False)
variation_id = db.Column(db.Integer)
__table_args__ = (
db.ForeignKeyConstraint([product_id], ['product.id']),
db.ForeignKeyConstraint([product_id, variation_id],
['variation.product_id', 'variation.id']),
)
由于外键默认为 MATCH SIMPLE,变体的复合外键将允许添加变体 ID 为 NULL 的行,但如果给定变体 ID,则组合必须引用现有行。此设置允许将现有关系 product_in_order 和 variation_in_order 用于 Product
和 Variation
分别代替下面涉及更多的模型,尽管 SQLAlchemy 会(正确地)警告这些关系存在冲突,因为它们都设置了产品 ID。创建订单时只需使用其中之一:
In [24]: o1 = Order(product_in_order=product)
In [25]: o2 = Order(variation_in_order=variation)
或跟随documentation about resolving the conflict。在此模型中,产品名称始终可用
In [31]: o1.product_in_order.name
另一种防止在给定产品时向订单添加不相关变体的选项是在这种情况下完全避免添加变体,反之亦然:
class Order(db.Model):
...
variation_id = db.Column(db.Integer, db.ForeignKey('variation.id'))
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
__table_args__ = (
# Require either a variation or a product
db.CheckConstraint(
'(variation_id IS NOT NULL AND product_id IS NULL) OR '
'(variation_id IS NULL AND product_id IS NOT NULL)'),
)
建立与 Product
的关系在此模型中有点复杂,需要 using a non primary mapper:
product_variation = db.outerjoin(
Product, db.select([Variation.id,
Variation.product_id]).alias('variation'))
ProductVariation = db.mapper(
Product, product_variation, non_primary=True,
properties={
'id': [product_variation.c.product_id,
product_variation.c.variation_product_id],
'variation_id': product_variation.c.variation_id
})
连接生成的可选对象映射回 Product
,但也允许基于 Variation.id
选择:
Order.product = db.relationship(
ProductVariation,
primaryjoin=db.or_(Order.product_id == ProductVariation.c.id,
Order.variation_id == ProductVariation.c.variation_id))
这样您就可以使用
从Order
实例访问产品名称
order.product.name
演示:
In [2]: p1 = Product(name='Product 1')
In [3]: v11 = Variation(product=p1)
In [4]: v12 = Variation(product=p1)
In [5]: p2 = Product(name='Product 2')
In [6]: v21 = Variation(product=p2)
In [9]: session.add_all([p1, p2])
In [10]: session.add_all([v11, v12, v21])
In [11]: session.commit()
In [12]: o1 = Order(product_id=p1.id)
In [13]: o2 = Order(variation_id=v12.id)
In [14]: o3 = Order(variation_id=v11.id)
In [15]: o4 = Order(product_id=p2.id)
In [16]: o5 = Order(variation_id=v21.id)
In [17]: session.add_all([o1, o2, o3, o4, o5])
In [18]: session.commit()
In [25]: [o.product.name for o in session.query(Order).all()]
Out[25]: ['Product 1', 'Product 1', 'Product 1', 'Product 2', 'Product 2']
LEFT JOIN 确保没有变化的产品也能正常工作:
In [26]: p3 = Product(name='Product 3')
In [27]: session.add(p3)
In [28]: session.commit()
In [29]: session.add(Order(product_id=p3.id))
In [30]: session.commit()
In [31]: [o.product.name for o in session.query(Order).all()]
Out[31]: ['Product 1', 'Product 1', 'Product 1', 'Product 2', 'Product 2', 'Product 3']
另一方面,您可以使用所描述的 CheckConstraint
和普通的 property
:
class Order(db.Model):
...
@property
def product(self):
if self.product_in_order:
return self.product_in_order
else:
return self.variation_in_order.origin_product
请注意,如果没有预先加载,这将在变化顺序的情况下针对数据库触发 2 个单独的 SELECT 查询。