mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-06-01 05:18:44 -04:00
- The subquery wrapping which occurs when joined eager loading
is used with a one-to-many query that also features LIMIT, OFFSET, or DISTINCT has been disabled in the case of a one-to-one relationship, that is a one-to-many with :paramref:`.relationship.uselist` set to False. This will produce more efficient queries in these cases. fixes #3249
This commit is contained in:
Vendored
+16
@@ -23,6 +23,22 @@
|
||||
series as well. For changes that are specific to 1.0 with an emphasis
|
||||
on compatibility concerns, see :doc:`/changelog/migration_10`.
|
||||
|
||||
.. change::
|
||||
:tags: feature, orm
|
||||
:tickets: 3249
|
||||
|
||||
The subquery wrapping which occurs when joined eager loading
|
||||
is used with a one-to-many query that also features LIMIT,
|
||||
OFFSET, or DISTINCT has been disabled in the case of a one-to-one
|
||||
relationship, that is a one-to-many with
|
||||
:paramref:`.relationship.uselist` set to False. This will produce
|
||||
more efficient queries in these cases.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`change_3249`
|
||||
|
||||
|
||||
.. change::
|
||||
:tags: bug, orm
|
||||
:tickets: 3301
|
||||
|
||||
Vendored
+49
@@ -1203,6 +1203,55 @@ join into a subquery as a join target on SQLite.
|
||||
|
||||
:ticket:`3008`
|
||||
|
||||
.. _change_3429:
|
||||
|
||||
Subqueries no longer applied to uselist=False joined eager loads
|
||||
----------------------------------------------------------------
|
||||
|
||||
Given a joined eager load like the following::
|
||||
|
||||
class A(Base):
|
||||
__tablename__ = 'a'
|
||||
id = Column(Integer, primary_key=True)
|
||||
b = relationship("B", uselist=False)
|
||||
|
||||
|
||||
class B(Base):
|
||||
__tablename__ = 'b'
|
||||
id = Column(Integer, primary_key=True)
|
||||
a_id = Column(ForeignKey('a.id'))
|
||||
|
||||
s = Session()
|
||||
print(s.query(A).options(joinedload(A.b)).limit(5))
|
||||
|
||||
SQLAlchemy considers the relationship ``A.b`` to be a "one to many,
|
||||
loaded as a single value", which is essentially a "one to one"
|
||||
relationship. However, joined eager loading has always treated the
|
||||
above as a situation where the main query needs to be inside a
|
||||
subquery, as would normally be needed for a collection of B objects
|
||||
where the main query has a LIMIT applied::
|
||||
|
||||
SELECT anon_1.a_id AS anon_1_a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
|
||||
FROM (SELECT a.id AS a_id
|
||||
FROM a LIMIT :param_1) AS anon_1
|
||||
LEFT OUTER JOIN b AS b_1 ON anon_1.a_id = b_1.a_id
|
||||
|
||||
However, since the relationship of the inner query to the outer one is
|
||||
that at most only one row is shared in the case of ``uselist=False``
|
||||
(in the same way as a many-to-one), the "subquery" used with LIMIT +
|
||||
joined eager loading is now dropped in this case::
|
||||
|
||||
SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
|
||||
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
|
||||
LIMIT :param_1
|
||||
|
||||
In the case that the LEFT OUTER JOIN returns more than one row, the ORM
|
||||
has always emitted a warning here and ignored addtional results for
|
||||
``uselist=False``, so the results in that error situation should not change.
|
||||
|
||||
:ticket:`3249`
|
||||
|
||||
|
||||
query.update() with ``synchronize_session='evaluate'`` raises on multi-table update
|
||||
-----------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1269,7 +1269,7 @@ class JoinedLoader(AbstractRelationshipLoader):
|
||||
anonymize_labels=True)
|
||||
assert clauses.aliased_class is not None
|
||||
|
||||
if self.parent_property.direction != interfaces.MANYTOONE:
|
||||
if self.parent_property.uselist:
|
||||
context.multi_row_eager_loaders = True
|
||||
|
||||
innerjoin = (
|
||||
|
||||
@@ -571,20 +571,20 @@ class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL):
|
||||
# test that the splicing of the join works here, doesn't break in
|
||||
# the middle of "parent join child1"
|
||||
q = sess.query(Child1).options(joinedload('left_child2'))
|
||||
self.assert_compile(q.limit(1).with_labels().statement,
|
||||
"SELECT anon_1.child1_id AS anon_1_child1_id, anon_1.parent_id "
|
||||
"AS anon_1_parent_id, anon_1.parent_cls AS anon_1_parent_cls, "
|
||||
"child2_1.id AS child2_1_id, parent_1.id AS "
|
||||
"parent_1_id, parent_1.cls AS parent_1_cls FROM "
|
||||
"(SELECT child1.id AS child1_id, parent.id AS parent_id, "
|
||||
"parent.cls AS parent_cls "
|
||||
self.assert_compile(
|
||||
q.limit(1).with_labels().statement,
|
||||
"SELECT child1.id AS child1_id, parent.id AS parent_id, "
|
||||
"parent.cls AS parent_cls, child2_1.id AS child2_1_id, "
|
||||
"parent_1.id AS parent_1_id, parent_1.cls AS parent_1_cls "
|
||||
"FROM parent JOIN child1 ON parent.id = child1.id "
|
||||
"LIMIT :param_1) AS anon_1 LEFT OUTER JOIN "
|
||||
"(secondary AS secondary_1 JOIN "
|
||||
"LEFT OUTER JOIN (secondary AS secondary_1 JOIN "
|
||||
"(parent AS parent_1 JOIN child2 AS child2_1 "
|
||||
"ON parent_1.id = child2_1.id) ON parent_1.id = secondary_1.left_id) "
|
||||
"ON anon_1.parent_id = secondary_1.right_id",
|
||||
{'param_1':1})
|
||||
"ON parent_1.id = child2_1.id) "
|
||||
"ON parent_1.id = secondary_1.left_id) "
|
||||
"ON parent.id = secondary_1.right_id "
|
||||
"LIMIT :param_1",
|
||||
checkparams={'param_1': 1}
|
||||
)
|
||||
|
||||
# another way to check
|
||||
assert q.limit(1).with_labels().subquery().count().scalar() == 1
|
||||
|
||||
@@ -1073,6 +1073,32 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
|
||||
eq_([User(id=7, address=Address(id=1))], l)
|
||||
self.assert_sql_count(testing.db, go, 1)
|
||||
|
||||
def test_one_to_many_scalar_subq_wrapping(self):
|
||||
Address, addresses, users, User = (self.classes.Address,
|
||||
self.tables.addresses,
|
||||
self.tables.users,
|
||||
self.classes.User)
|
||||
|
||||
mapper(User, users, properties=dict(
|
||||
address=relationship(mapper(Address, addresses),
|
||||
lazy='joined', uselist=False)
|
||||
))
|
||||
q = create_session().query(User)
|
||||
q = q.filter(users.c.id == 7).limit(1)
|
||||
|
||||
self.assert_compile(
|
||||
q,
|
||||
"SELECT users.id AS users_id, users.name AS users_name, "
|
||||
"addresses_1.id AS addresses_1_id, "
|
||||
"addresses_1.user_id AS addresses_1_user_id, "
|
||||
"addresses_1.email_address AS addresses_1_email_address "
|
||||
"FROM users LEFT OUTER JOIN addresses AS addresses_1 "
|
||||
"ON users.id = addresses_1.user_id "
|
||||
"WHERE users.id = :id_1 "
|
||||
"LIMIT :param_1",
|
||||
checkparams={'id_1': 7, 'param_1': 1}
|
||||
)
|
||||
|
||||
def test_many_to_one(self):
|
||||
users, Address, addresses, User = (
|
||||
self.tables.users,
|
||||
|
||||
Reference in New Issue
Block a user