Flask SQLAlchemy: Как добавить столбец, который зависит от другой таблицы? - PullRequest
0 голосов
/ 28 июня 2018

У меня есть три таблицы в моей базе данных SQLAlchemy (с использованием Flask SQLAlchemy): продукт, вариант продукта и заказ. Я хочу видеть в заказе, какой продукт и его разновидность включены.

Хорошо работает со связями / внешними ключами, но главная проблема: если я добавлю товар в заказ, я все равно смогу добавить вариант из другого товара (используя Flask-Admin или просто оболочку фляги).

Итак, главный вопрос: как создать связь между таблицами, чтобы вариации можно было добавлять, только если они являются вариациями продукта заказа? Спасибо:)

Другое решение: как добавить столбец в таблицу «Заказы», ​​чтобы он получал название продукта из таблицы «Продукт» на основе идентификатора варианта? Я пытался использовать column_property, Post.query.get(variation_id), variation.parent_id, backhref variation.origin_product, но безуспешно:)

Мои модели:

Продукт (например, Samsung 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 32 ГБ)

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')) работает в дБ, я вижу правильный идентификатор продукта. Тем не менее внешние инструменты, такие как Flask-Admin, видят столбец product_id в качестве объекта варианта, поэтому бесполезно. Нужен способ, чтобы подключить объект товара к product_id. Мол, подключите продукт ForeignKey, но на основе variation_id.

1 Ответ

0 голосов
/ 29 июня 2018

Одним из способов предотвращения несвязанных комбинаций продуктов и вариантов было бы создание внешнего ключа от заказа к продукту и перекрывающегося составного внешнего ключа от заказа к варианту. Чтобы иметь возможность ссылаться на комбинацию 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 к базе данных в случае заказа на изменение.

...