sqlalchemy, select objects that has all tags
I have sqlalchemy models:
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)
I need to select all pizza objects which have the required set of topics:
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()
This returns all pizza that have one of topics. If I try to replace any
with all
, it doesn't work.
I've found that it is possible to make a query with JOIN and COUNT, but I couldn't build sqlalchemy query. Any possible solution will suit me.
python sqlalchemy many-to-many
add a comment |
I have sqlalchemy models:
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)
I need to select all pizza objects which have the required set of topics:
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()
This returns all pizza that have one of topics. If I try to replace any
with all
, it doesn't work.
I've found that it is possible to make a query with JOIN and COUNT, but I couldn't build sqlalchemy query. Any possible solution will suit me.
python sqlalchemy many-to-many
Are you open to changes in the structure of your models, or is the structure set and cannot be changed?
– SuperShoot
Nov 11 '18 at 21:55
I can change the structure.
– Iren
Nov 12 '18 at 5:15
By "required set" do you mean that you wan't pizzas with given toppings only, or with given toppings and possibly some others as well.
– Ilja Everilä
Nov 12 '18 at 5:46
I want pizza with given toppings and possibly some others.
– Iren
Nov 12 '18 at 5:53
Related: stackoverflow.com/questions/49438529/…, stackoverflow.com/questions/42673699/…
– Ilja Everilä
Nov 12 '18 at 12:30
add a comment |
I have sqlalchemy models:
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)
I need to select all pizza objects which have the required set of topics:
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()
This returns all pizza that have one of topics. If I try to replace any
with all
, it doesn't work.
I've found that it is possible to make a query with JOIN and COUNT, but I couldn't build sqlalchemy query. Any possible solution will suit me.
python sqlalchemy many-to-many
I have sqlalchemy models:
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)
I need to select all pizza objects which have the required set of topics:
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()
This returns all pizza that have one of topics. If I try to replace any
with all
, it doesn't work.
I've found that it is possible to make a query with JOIN and COUNT, but I couldn't build sqlalchemy query. Any possible solution will suit me.
python sqlalchemy many-to-many
python sqlalchemy many-to-many
edited Nov 12 '18 at 11:46
Ilja Everilä
23.4k33560
23.4k33560
asked Nov 11 '18 at 20:09
Iren
134
134
Are you open to changes in the structure of your models, or is the structure set and cannot be changed?
– SuperShoot
Nov 11 '18 at 21:55
I can change the structure.
– Iren
Nov 12 '18 at 5:15
By "required set" do you mean that you wan't pizzas with given toppings only, or with given toppings and possibly some others as well.
– Ilja Everilä
Nov 12 '18 at 5:46
I want pizza with given toppings and possibly some others.
– Iren
Nov 12 '18 at 5:53
Related: stackoverflow.com/questions/49438529/…, stackoverflow.com/questions/42673699/…
– Ilja Everilä
Nov 12 '18 at 12:30
add a comment |
Are you open to changes in the structure of your models, or is the structure set and cannot be changed?
– SuperShoot
Nov 11 '18 at 21:55
I can change the structure.
– Iren
Nov 12 '18 at 5:15
By "required set" do you mean that you wan't pizzas with given toppings only, or with given toppings and possibly some others as well.
– Ilja Everilä
Nov 12 '18 at 5:46
I want pizza with given toppings and possibly some others.
– Iren
Nov 12 '18 at 5:53
Related: stackoverflow.com/questions/49438529/…, stackoverflow.com/questions/42673699/…
– Ilja Everilä
Nov 12 '18 at 12:30
Are you open to changes in the structure of your models, or is the structure set and cannot be changed?
– SuperShoot
Nov 11 '18 at 21:55
Are you open to changes in the structure of your models, or is the structure set and cannot be changed?
– SuperShoot
Nov 11 '18 at 21:55
I can change the structure.
– Iren
Nov 12 '18 at 5:15
I can change the structure.
– Iren
Nov 12 '18 at 5:15
By "required set" do you mean that you wan't pizzas with given toppings only, or with given toppings and possibly some others as well.
– Ilja Everilä
Nov 12 '18 at 5:46
By "required set" do you mean that you wan't pizzas with given toppings only, or with given toppings and possibly some others as well.
– Ilja Everilä
Nov 12 '18 at 5:46
I want pizza with given toppings and possibly some others.
– Iren
Nov 12 '18 at 5:53
I want pizza with given toppings and possibly some others.
– Iren
Nov 12 '18 at 5:53
Related: stackoverflow.com/questions/49438529/…, stackoverflow.com/questions/42673699/…
– Ilja Everilä
Nov 12 '18 at 12:30
Related: stackoverflow.com/questions/49438529/…, stackoverflow.com/questions/42673699/…
– Ilja Everilä
Nov 12 '18 at 12:30
add a comment |
2 Answers
2
active
oldest
votes
Firstly, there is a stack of reading you can do about SQLAlchemy relationships in the docs.
Your code closely matches the Association Object
pattern which is (from the docs):
...used when your association table contains additional columns beyond those which are foreign keys to the left and right tables
I.e., if there was something specific about the individual relationship between a Pizza
and Topic
, you would store that information in line with the relationship between foreign keys in the association table. Here's the example that the docs give:
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")
Note the column extra_data
defined on the Association
object.
In your example, there is no such need for an extra_data type field in Association
so you can simplify expressing the relationship between Pizza
and Topic
by using the Many to Many Pattern outlined in the docs.
The main benefit that we can get from that pattern is that we can directly relate the Pizza
class to the Topic
class. The new models look something like this:
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
The differences to your original code are:
- No relationships defined on the
TopicToPizzaAssociation
model. With this pattern we can directly relatePizza
toTopic
without having relationships on the association model. - Added
__repr__()
methods to both models so they print nicer. - Removed the
add_topics
method fromPizza
(will explain that more later). - Added the
secondary='association'
argument toPizza.topics
relationship. This tells sqlalchemy that the foreign key path needed for the relationship toTopic
is through theassociation
table.
Here is the testing code and I've put some comments in there:
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]
So this only returns pizzas that have all of the prescribed topics, but not only the prescribed topics.
The reason I left out your add_topics
method is that you used that method to check for duplicate Topics
added to a given Pizza
. That's fine, but the primary key of the association table won't let you add a duplicate topic for a pizza anyway, so I think it's better to let the database layer manage that and just handle the exception that occurs in application code.
add a comment |
A query to fetch all Pizza
with given Topic
s (and possibly more) can be expressed using a slightly hard to read double negation:
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())
In English it reads along the lines of "Fetch pizza where not exists a given topic [sic] that is not in this pizza."
This returns all pizza that have one of topics. If I try to replace
any
withall
, it doesn't work.
SQL does not have universal quantification and so there is no all()
operator for relationships the way that any()
maps to EXISTS
. But
FORALL x ( p ( x ) )
is logically equivalent to
NOT EXISTS x ( NOT p ( x ) )
which the above query takes advantage of. It is also described as how to perform relational division in SQL:
- https://www.red-gate.com/simple-talk/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/
- https://www.jooq.org/doc/3.11/manual/sql-building/table-expressions/relational-division/
- http://www.inf.usi.ch/faculty/soule/teaching/2016-fall/db/division.pdf
add a comment |
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "1"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53252749%2fsqlalchemy-select-objects-that-has-all-tags%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
Firstly, there is a stack of reading you can do about SQLAlchemy relationships in the docs.
Your code closely matches the Association Object
pattern which is (from the docs):
...used when your association table contains additional columns beyond those which are foreign keys to the left and right tables
I.e., if there was something specific about the individual relationship between a Pizza
and Topic
, you would store that information in line with the relationship between foreign keys in the association table. Here's the example that the docs give:
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")
Note the column extra_data
defined on the Association
object.
In your example, there is no such need for an extra_data type field in Association
so you can simplify expressing the relationship between Pizza
and Topic
by using the Many to Many Pattern outlined in the docs.
The main benefit that we can get from that pattern is that we can directly relate the Pizza
class to the Topic
class. The new models look something like this:
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
The differences to your original code are:
- No relationships defined on the
TopicToPizzaAssociation
model. With this pattern we can directly relatePizza
toTopic
without having relationships on the association model. - Added
__repr__()
methods to both models so they print nicer. - Removed the
add_topics
method fromPizza
(will explain that more later). - Added the
secondary='association'
argument toPizza.topics
relationship. This tells sqlalchemy that the foreign key path needed for the relationship toTopic
is through theassociation
table.
Here is the testing code and I've put some comments in there:
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]
So this only returns pizzas that have all of the prescribed topics, but not only the prescribed topics.
The reason I left out your add_topics
method is that you used that method to check for duplicate Topics
added to a given Pizza
. That's fine, but the primary key of the association table won't let you add a duplicate topic for a pizza anyway, so I think it's better to let the database layer manage that and just handle the exception that occurs in application code.
add a comment |
Firstly, there is a stack of reading you can do about SQLAlchemy relationships in the docs.
Your code closely matches the Association Object
pattern which is (from the docs):
...used when your association table contains additional columns beyond those which are foreign keys to the left and right tables
I.e., if there was something specific about the individual relationship between a Pizza
and Topic
, you would store that information in line with the relationship between foreign keys in the association table. Here's the example that the docs give:
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")
Note the column extra_data
defined on the Association
object.
In your example, there is no such need for an extra_data type field in Association
so you can simplify expressing the relationship between Pizza
and Topic
by using the Many to Many Pattern outlined in the docs.
The main benefit that we can get from that pattern is that we can directly relate the Pizza
class to the Topic
class. The new models look something like this:
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
The differences to your original code are:
- No relationships defined on the
TopicToPizzaAssociation
model. With this pattern we can directly relatePizza
toTopic
without having relationships on the association model. - Added
__repr__()
methods to both models so they print nicer. - Removed the
add_topics
method fromPizza
(will explain that more later). - Added the
secondary='association'
argument toPizza.topics
relationship. This tells sqlalchemy that the foreign key path needed for the relationship toTopic
is through theassociation
table.
Here is the testing code and I've put some comments in there:
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]
So this only returns pizzas that have all of the prescribed topics, but not only the prescribed topics.
The reason I left out your add_topics
method is that you used that method to check for duplicate Topics
added to a given Pizza
. That's fine, but the primary key of the association table won't let you add a duplicate topic for a pizza anyway, so I think it's better to let the database layer manage that and just handle the exception that occurs in application code.
add a comment |
Firstly, there is a stack of reading you can do about SQLAlchemy relationships in the docs.
Your code closely matches the Association Object
pattern which is (from the docs):
...used when your association table contains additional columns beyond those which are foreign keys to the left and right tables
I.e., if there was something specific about the individual relationship between a Pizza
and Topic
, you would store that information in line with the relationship between foreign keys in the association table. Here's the example that the docs give:
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")
Note the column extra_data
defined on the Association
object.
In your example, there is no such need for an extra_data type field in Association
so you can simplify expressing the relationship between Pizza
and Topic
by using the Many to Many Pattern outlined in the docs.
The main benefit that we can get from that pattern is that we can directly relate the Pizza
class to the Topic
class. The new models look something like this:
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
The differences to your original code are:
- No relationships defined on the
TopicToPizzaAssociation
model. With this pattern we can directly relatePizza
toTopic
without having relationships on the association model. - Added
__repr__()
methods to both models so they print nicer. - Removed the
add_topics
method fromPizza
(will explain that more later). - Added the
secondary='association'
argument toPizza.topics
relationship. This tells sqlalchemy that the foreign key path needed for the relationship toTopic
is through theassociation
table.
Here is the testing code and I've put some comments in there:
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]
So this only returns pizzas that have all of the prescribed topics, but not only the prescribed topics.
The reason I left out your add_topics
method is that you used that method to check for duplicate Topics
added to a given Pizza
. That's fine, but the primary key of the association table won't let you add a duplicate topic for a pizza anyway, so I think it's better to let the database layer manage that and just handle the exception that occurs in application code.
Firstly, there is a stack of reading you can do about SQLAlchemy relationships in the docs.
Your code closely matches the Association Object
pattern which is (from the docs):
...used when your association table contains additional columns beyond those which are foreign keys to the left and right tables
I.e., if there was something specific about the individual relationship between a Pizza
and Topic
, you would store that information in line with the relationship between foreign keys in the association table. Here's the example that the docs give:
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")
Note the column extra_data
defined on the Association
object.
In your example, there is no such need for an extra_data type field in Association
so you can simplify expressing the relationship between Pizza
and Topic
by using the Many to Many Pattern outlined in the docs.
The main benefit that we can get from that pattern is that we can directly relate the Pizza
class to the Topic
class. The new models look something like this:
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
The differences to your original code are:
- No relationships defined on the
TopicToPizzaAssociation
model. With this pattern we can directly relatePizza
toTopic
without having relationships on the association model. - Added
__repr__()
methods to both models so they print nicer. - Removed the
add_topics
method fromPizza
(will explain that more later). - Added the
secondary='association'
argument toPizza.topics
relationship. This tells sqlalchemy that the foreign key path needed for the relationship toTopic
is through theassociation
table.
Here is the testing code and I've put some comments in there:
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]
So this only returns pizzas that have all of the prescribed topics, but not only the prescribed topics.
The reason I left out your add_topics
method is that you used that method to check for duplicate Topics
added to a given Pizza
. That's fine, but the primary key of the association table won't let you add a duplicate topic for a pizza anyway, so I think it's better to let the database layer manage that and just handle the exception that occurs in application code.
edited Nov 12 '18 at 6:01
answered Nov 12 '18 at 5:55
SuperShoot
1,585619
1,585619
add a comment |
add a comment |
A query to fetch all Pizza
with given Topic
s (and possibly more) can be expressed using a slightly hard to read double negation:
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())
In English it reads along the lines of "Fetch pizza where not exists a given topic [sic] that is not in this pizza."
This returns all pizza that have one of topics. If I try to replace
any
withall
, it doesn't work.
SQL does not have universal quantification and so there is no all()
operator for relationships the way that any()
maps to EXISTS
. But
FORALL x ( p ( x ) )
is logically equivalent to
NOT EXISTS x ( NOT p ( x ) )
which the above query takes advantage of. It is also described as how to perform relational division in SQL:
- https://www.red-gate.com/simple-talk/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/
- https://www.jooq.org/doc/3.11/manual/sql-building/table-expressions/relational-division/
- http://www.inf.usi.ch/faculty/soule/teaching/2016-fall/db/division.pdf
add a comment |
A query to fetch all Pizza
with given Topic
s (and possibly more) can be expressed using a slightly hard to read double negation:
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())
In English it reads along the lines of "Fetch pizza where not exists a given topic [sic] that is not in this pizza."
This returns all pizza that have one of topics. If I try to replace
any
withall
, it doesn't work.
SQL does not have universal quantification and so there is no all()
operator for relationships the way that any()
maps to EXISTS
. But
FORALL x ( p ( x ) )
is logically equivalent to
NOT EXISTS x ( NOT p ( x ) )
which the above query takes advantage of. It is also described as how to perform relational division in SQL:
- https://www.red-gate.com/simple-talk/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/
- https://www.jooq.org/doc/3.11/manual/sql-building/table-expressions/relational-division/
- http://www.inf.usi.ch/faculty/soule/teaching/2016-fall/db/division.pdf
add a comment |
A query to fetch all Pizza
with given Topic
s (and possibly more) can be expressed using a slightly hard to read double negation:
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())
In English it reads along the lines of "Fetch pizza where not exists a given topic [sic] that is not in this pizza."
This returns all pizza that have one of topics. If I try to replace
any
withall
, it doesn't work.
SQL does not have universal quantification and so there is no all()
operator for relationships the way that any()
maps to EXISTS
. But
FORALL x ( p ( x ) )
is logically equivalent to
NOT EXISTS x ( NOT p ( x ) )
which the above query takes advantage of. It is also described as how to perform relational division in SQL:
- https://www.red-gate.com/simple-talk/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/
- https://www.jooq.org/doc/3.11/manual/sql-building/table-expressions/relational-division/
- http://www.inf.usi.ch/faculty/soule/teaching/2016-fall/db/division.pdf
A query to fetch all Pizza
with given Topic
s (and possibly more) can be expressed using a slightly hard to read double negation:
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())
In English it reads along the lines of "Fetch pizza where not exists a given topic [sic] that is not in this pizza."
This returns all pizza that have one of topics. If I try to replace
any
withall
, it doesn't work.
SQL does not have universal quantification and so there is no all()
operator for relationships the way that any()
maps to EXISTS
. But
FORALL x ( p ( x ) )
is logically equivalent to
NOT EXISTS x ( NOT p ( x ) )
which the above query takes advantage of. It is also described as how to perform relational division in SQL:
- https://www.red-gate.com/simple-talk/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/
- https://www.jooq.org/doc/3.11/manual/sql-building/table-expressions/relational-division/
- http://www.inf.usi.ch/faculty/soule/teaching/2016-fall/db/division.pdf
edited Nov 12 '18 at 12:38
answered Nov 12 '18 at 12:20
Ilja Everilä
23.4k33560
23.4k33560
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53252749%2fsqlalchemy-select-objects-that-has-all-tags%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Are you open to changes in the structure of your models, or is the structure set and cannot be changed?
– SuperShoot
Nov 11 '18 at 21:55
I can change the structure.
– Iren
Nov 12 '18 at 5:15
By "required set" do you mean that you wan't pizzas with given toppings only, or with given toppings and possibly some others as well.
– Ilja Everilä
Nov 12 '18 at 5:46
I want pizza with given toppings and possibly some others.
– Iren
Nov 12 '18 at 5:53
Related: stackoverflow.com/questions/49438529/…, stackoverflow.com/questions/42673699/…
– Ilja Everilä
Nov 12 '18 at 12:30