One way to prevent unrelated product and variation combinations would be to create a foreign key from order to product and an overlapping composite foreign key from order to variation. In order to be able to reference the combination of variation.id, variation.product_id
the product id should be made a part of the primary key as well and id must be given auto incrementing behaviour explicitly:
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']),
)
Since a foreign key defaults to MATCH SIMPLE the composite foreign key to variation will allow adding rows where variation id is NULL, but if variation id is given, the combination must reference an existing row. This setup allows using the existing relationships product_in_order and variation_in_order to both Product
and Variation
respectively instead of the more involved models below, though SQLAlchemy will (rightly) warn about the fact that the relationships have a conflict in that they both set product id. Just use one of them when creating orders:
In [24]: o1 = Order(product_in_order=product)
In [25]: o2 = Order(variation_in_order=variation)
or follow the documentation about resolving the conflict. In this model the product name is always available as
In [31]: o1.product_in_order.name
Another option to prevent adding unrelated variations to orders when the product is given is to prevent adding a variation entirely in that case, and vice versa:
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)'),
)
Building the relationship to Product
is a bit more complex in this model, and requires 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
})
The selectable produced by the join is mapped back to Product
, but allows selecting based on Variation.id
as well:
Order.product = db.relationship(
ProductVariation,
primaryjoin=db.or_(Order.product_id == ProductVariation.c.id,
Order.variation_id == ProductVariation.c.variation_id))
This way you can access the product name from an Order
instance with
order.product.name
Demo:
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']
The LEFT JOIN ensures that products without variations work as well:
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']
On the other hand instead of this rather complex construction you could use the CheckConstraint
as described and an ordinary 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
Just note that without eager loading this will fire 2 separate SELECT queries against the database in case of a variation order.