【问题标题】:sqlalchemy, select objects that has all tagssqlalchemy,选择具有所有标签的对象
【发布时间】:2019-04-14 14:50:28
【问题描述】:

我有 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 查询。任何可能的解决方案都适合我。

【问题讨论】:

  • 您对模型结构的更改持开放态度,还是结构已设置且无法更改?
  • 我可以改变结构。
  • 您所说的“必需设置”是指您不想要只有给定配料的比萨饼,还是不给定的配料,可能还有其他一些配料。
  • 我想要有给定配料的披萨,可能还有其他配料。

标签: python sqlalchemy many-to-many


【解决方案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")

注意Association 对象上定义的列extra_data

在您的示例中,Association 中不需要 extra_data 类型字段,因此您可以使用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__() 方法,以便打印效果更好。
  • Pizza 中删除了add_topics 方法(稍后会详细解释)。
  • secondary='association' 参数添加到Pizza.topics 关系。这告诉 sqlalchemy 与 Topic 的关系所需的外键路径是通过 association 表。

这是测试代码,我在里面放了一些 cmets:

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 方法的原因是您使用该方法来检查添加到给定Pizza 的重复Topics。没关系,但是关联表的主键无论如何都不会让你为披萨添加重复的主题,所以我认为最好让数据库层管理它,只处理应用程序代码中发生的异常。

【讨论】:

    【解决方案2】:

    使用给定的Topics(可能更多)获取所有Pizza的查询可以使用稍微难以阅读的双重否定来表示:

    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())
    

    在英语中,它的意思是“在不存在给定主题 [原文如此] 的地方获取披萨,但该主题不在此披萨中。”

    这将返回具有主题之一的所有披萨。如果我尝试将any 替换为all,它不起作用。

    SQL 没有universal quantification,因此没有all() 运算符用于关系,any() 映射到EXISTS。但是

    FORALL x ( p ( x ) )
    

    逻辑上等价于

    NOT EXISTS x ( NOT p ( x ) )
    

    上面的查询利用的。还描述了如何在SQL中执行relational division

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-04
      • 1970-01-01
      • 1970-01-01
      • 2018-12-04
      • 2023-03-23
      • 2015-10-06
      • 1970-01-01
      相关资源
      最近更新 更多