sqlalchemy,选择具有所有标签的对象

时间:2018-11-11 20:09:18

标签: python sqlalchemy many-to-many

我有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 :(得分:1)

首先,您可以阅读大量有关SQLAlchemy关系in the docs的文章。

您的代码与Association Object模式非常匹配(来自文档):

  

...当关联表包含除左,右表外键以外的其他列时使用

也就是说,如果PizzaTopic之间的个别关系有一些特定的内容,则可以将该信息存储为与关联表中外键之间的关系一致的信息。这是文档提供的示例:

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

在您的示例中,Association中不需要Extra_data type字段,因此您可以使用Many to Many Pattern outlined in the docs简化表达PizzaTopic之间的关系。

我们可以从该模式中获得的主要好处是,我们可以将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模型上未定义任何关系。通过这种模式,我们可以直接将PizzaTopic进行关联,而无需在关联模型上建立关系。
  • 在两个模型中都添加了__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。很好,但是关联表的主键无论如何都不允许您为披萨添加重复的主题,因此我认为最好由数据库层来管理它并仅处理应用程序代码中发生的异常。

答案 1 :(得分:0)

使用给定的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没有universal quantification,因此没有all()运算符来处理any()映射到EXISTS的关系。但是

FORALL x ( p ( x ) )

在逻辑上等同于

NOT EXISTS x ( NOT p ( x ) )

上述查询可利用的

。它也被描述为如何在SQL中执行relational division