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