sqlalchemy, выберите объекты, которые имеют все теги - PullRequest
0 голосов
/ 11 ноября 2018

У меня есть модели sqlalchemy:

import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, and_
from sqlalchemy.orm import sessionmaker, relationship


engine = create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
Base = declarative_base()

class TopicToPizzaAssociation(Base):
    __tablename__ = 'association'
    pizza_id = Column(Integer, ForeignKey('pizza.id'), primary_key=True)
    topic_id = Column(Integer, ForeignKey('topic.id'), primary_key=True)
    topic = relationship("Topic")
    pizza = relationship("Pizza")

class Pizza(Base):
    __tablename__ = 'pizza'
    id = Column(Integer, primary_key=True)
    topics = relationship("TopicToPizzaAssociation")

    def add_topics(self, topics):
        used_topics = {t.topic.product for t in self.topics}
        associations = []
        for topic in topics:
            if topic.product not in used_topics:
                associations.append(TopicToPizzaAssociation(pizza=self, topic=topic))
                used_topics.add(topic.product)
        p1.topics.extend(associations)

class Topic(Base):
    __tablename__ = 'topic'
    id = Column(Integer, primary_key=True)
    product = Column(String(), nullable=False)

Мне нужно выбрать все объекты для пиццы, которые имеют необходимый набор тем:

t1 = Topic(product='t1')
t2 = Topic(product='t2')
t3 = Topic(product='t3')

session = Session()
session.add_all([t1, t2, t3])

p1 = Pizza()
p2 = Pizza()

p1.add_topics([t1, t2, t1])
p2.add_topics([t2, t3])

Base.metadata.create_all(engine)

session.add_all([p1, p2])
session.commit()

values = ['t1', 't2']
topics = session.query(Topic.id).filter(Topic.product.in_(values))
pizza = session.query(Pizza).filter(Pizza.topics.any(TopicToPizzaAssociation.topic_id.in_(
    topics
))).all()

Возвращает всю пиццу с одной из тем. Если я попытаюсь заменить any на all, это не сработает.

Я обнаружил, что можно выполнить запрос с помощью JOIN и COUNT, но я не смог построить запрос sqlalchemy. Любое возможное решение подойдет мне.

Ответы [ 2 ]

0 голосов
/ 12 ноября 2018

Запрос на выборку всех Pizza с заданными Topic с (и, возможно, более) может быть выражен с использованием слегка трудного для чтения двойного отрицания:

session.query(Pizza).\
    filter(~session.query(Topic).
           filter(Topic.product.in_(values),
                  ~session.query(TopicToPizzaAssociation).
                  filter(TopicToPizzaAssociation.topic_id == Topic.id,
                         TopicToPizzaAssociation.pizza_id == Pizza.id).
                  correlate(Pizza, Topic).
                  exists()).
           exists())

На английском языке это звучит так: «Получить пиццу, где не существует заданная тема [sic], которой нет в этой пицце».

Возвращает всю пиццу с одной из тем. Если я попытаюсь заменить any на all, это не сработает.

SQL не имеет универсального количественного определения , поэтому не существует оператора all() для отношений, которые any() отображаются на EXISTS. Но

FORALL x ( p ( x ) )

логически эквивалентно

NOT EXISTS x ( NOT p ( x ) )

, который использует вышеуказанный запрос. Также описывается, как выполнить реляционное деление в SQL:

0 голосов
/ 12 ноября 2018

Во-первых, есть стек чтения, который вы можете сделать с отношениями SQLAlchemy в документах .

Ваш код близко соответствует шаблону Association Object, который (из документов):

... используется, когда ваша таблица сопоставления содержит дополнительные столбцы помимо тех, которые являются внешними ключами для левой и правой таблиц

Т.е., если бы было что-то конкретное в индивидуальных отношениях между Pizza и Topic, вы бы сохранили эту информацию в соответствии с отношением между внешними ключами в таблице ассоциации. Вот пример, который дают документы:

class Association(Base):
    __tablename__ = 'association'
    left_id = Column(Integer, ForeignKey('left.id'), primary_key=True)
    right_id = Column(Integer, ForeignKey('right.id'), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child", back_populates="parents")
    parent = relationship("Parent", back_populates="children")

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Association", back_populates="parent")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)
    parents = relationship("Association", back_populates="child")

Обратите внимание на столбец extra_data, определенный для объекта Association.

В вашем примере нет необходимости в поле типа extra_data в Association, поэтому вы можете упростить выражение взаимосвязи между Pizza и Topic, используя шаблон «Многие ко многим», описанный в документации. .

Основное преимущество, которое мы можем получить от этого шаблона, заключается в том, что мы можем напрямую связать класс Pizza с классом Topic. Новые модели выглядят примерно так:

class TopicToPizzaAssociation(Base):
    __tablename__ = 'association'
    pizza_id = Column(Integer, ForeignKey('pizza.id'), primary_key=True)
    topic_id = Column(Integer, ForeignKey('topic.id'), primary_key=True)


class Pizza(Base):
    __tablename__ = 'pizza'
    id = Column(Integer, primary_key=True)
    topics = relationship("Topic", secondary='association')  # relationship is directly to Topic, not to the association table

    def __repr__(self):
        return f'pizza {self.id}'


class Topic(Base):
    __tablename__ = 'topic'
    id = Column(Integer, primary_key=True)
    product = Column(String(), nullable=False)

    def __repr__(self):
        return self.product

Отличия от вашего исходного кода:

  • В модели TopicToPizzaAssociation не определены отношения. С этим шаблоном мы можем напрямую связать Pizza с Topic, не имея связей с моделью ассоциации.
  • Добавлены __repr__() методы для обеих моделей, чтобы они печатались лучше.
  • Удален метод add_topics из Pizza (подробнее об этом позже).
  • Добавлен аргумент secondary='association' в отношение Pizza.topics. Это сообщает sqlalchemy, что путь внешнего ключа, необходимый для связи с Topic, проходит через таблицу association.

Вот код тестирования, и я добавил туда несколько комментариев:

t1 = Topic(product='t1')
t2 = Topic(product='t2')
t3 = Topic(product='t3')

session = Session()
session.add_all([t1, t2, t3])

p1 = Pizza()
p2 = Pizza()

p1.topics = [t1, t2]  # not adding to the pizzas through a add_topics method
p2.topics = [t2, t3]

Base.metadata.create_all(engine)

session.add_all([p1, p2])
session.commit()

values = [t2, t1]  # these aren't strings, but are the actual objects instantiated above

# using Pizza.topics.contains
print(session.query(Pizza).filter(*[Pizza.topics.contains(t) for t in values]).all())  # [pizza 1]

values = [t2, t3]
print(session.query(Pizza).filter(*[Pizza.topics.contains(t) for t in values]).all())  # [pizza 2]

values = [t2]
print(session.query(Pizza).filter(*[Pizza.topics.contains(t) for t in values]).all())  # [pizza 2, pizza 1]

Таким образом, это возвращает только пиццу, которая имеет все предписанные темы, но не только предписанные темы.

Причина, по которой я пропустил ваш метод add_topics, заключается в том, что вы использовали этот метод для проверки наличия дубликата Topics, добавленного к данному Pizza. Это нормально, но первичный ключ таблицы ассоциации не позволит вам в любом случае добавить повторяющуюся тему для пиццы, поэтому я думаю, что лучше, чтобы уровень базы данных управлял этим и просто обрабатывал исключение, возникающее в коде приложения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...