Одним из способов предотвращения несвязанных комбинаций продуктов и вариантов было бы создание внешнего ключа от заказа к продукту и перекрывающегося составного внешнего ключа от заказа к варианту. Чтобы иметь возможность ссылаться на комбинацию variation.id, variation.product_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, составной внешний ключ для варианта позволит добавлять строки, в которых идентификатор варианта равен NULL, но если указан идентификатор варианта, комбинация должна ссылаться на существующую строку. Эта настройка позволяет использовать существующие отношения product_in_order и var_in_order для Product
и Variation
соответственно вместо более задействованных моделей, указанных ниже, хотя SQLAlchemy будет (правильно) предупреждать о факте что отношения имеют конфликт в том, что они оба устанавливают идентификатор продукта Просто используйте один из них при создании заказов:
In [24]: o1 = Order(product_in_order=product)
In [25]: o2 = Order(variation_in_order=variation)
или следуйте документации по разрешению конфликта . В этой модели название продукта всегда доступно как
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
немного сложнее в этой модели и требует с использованием неосновного преобразователя :
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 к базе данных в случае заказа на изменение.