Skip to content

Commit 7e50367

Browse files
committed
disable batching for sqlalchemy < 1.2
1 parent d875c0e commit 7e50367

File tree

3 files changed

+36
-6
lines changed

3 files changed

+36
-6
lines changed

Diff for: graphene_sqlalchemy/tests/test_batching.py

+25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import contextlib
22
import logging
33

4+
import pkg_resources
5+
import pytest
6+
47
import graphene
58

69
from ..types import SQLAlchemyObjectType
@@ -78,6 +81,14 @@ def resolve_reporters(self, _info):
7881
return graphene.Schema(query=Query)
7982

8083

84+
def is_sqlalchemy_version_less_than(version_string):
85+
return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string)
86+
87+
88+
if is_sqlalchemy_version_less_than('1.2'):
89+
pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True)
90+
91+
8192
def test_many_to_one(session_factory):
8293
session = session_factory()
8394
make_fixture(session)
@@ -99,6 +110,13 @@ def test_many_to_one(session_factory):
99110
messages = sqlalchemy_logging_handler.messages
100111

101112
assert len(messages) == 5
113+
114+
if is_sqlalchemy_version_less_than('1.3'):
115+
# The batched SQL statement generated is different in 1.2.x
116+
# SQLAlchemy 1.3+ optimizes out a JOIN statement in `selectin`
117+
# See https://git.io/JewQu
118+
return
119+
102120
assert messages == [
103121
'BEGIN (implicit)',
104122

@@ -161,6 +179,13 @@ def test_one_to_one(session_factory):
161179
messages = sqlalchemy_logging_handler.messages
162180

163181
assert len(messages) == 5
182+
183+
if is_sqlalchemy_version_less_than('1.3'):
184+
# The batched SQL statement generated is different in 1.2.x
185+
# SQLAlchemy 1.3+ optimizes out a JOIN statement in `selectin`
186+
# See https://git.io/JewQu
187+
return
188+
164189
assert messages == [
165190
'BEGIN (implicit)',
166191

Diff for: graphene_sqlalchemy/types.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from promise import dataloader, promise
55
from sqlalchemy.ext.hybrid import hybrid_property
66
from sqlalchemy.orm import (ColumnProperty, CompositeProperty,
7-
RelationshipProperty, Session)
7+
RelationshipProperty, Session, strategies)
88
from sqlalchemy.orm.exc import NoResultFound
99
from sqlalchemy.orm.query import QueryContext
10-
from sqlalchemy.orm.strategies import SelectInLoader
1110

1211
from graphene import Field
1312
from graphene.relay import Connection, Node
@@ -213,6 +212,8 @@ def _get_custom_resolver(obj_type, orm_field_name):
213212
def _get_relationship_resolver(obj_type, relationship_prop, model_attr):
214213
"""
215214
Batch SQL queries using Dataloader to avoid the N+1 problem.
215+
SQL batching only works for SQLAlchemy 1.2+ since it depends on
216+
the `selectin` loader.
216217
217218
:param SQLAlchemyObjectType obj_type:
218219
:param sqlalchemy.orm.properties.RelationshipProperty relationship_prop:
@@ -222,7 +223,7 @@ def _get_relationship_resolver(obj_type, relationship_prop, model_attr):
222223
child_mapper = relationship_prop.mapper
223224
parent_mapper = relationship_prop.parent
224225

225-
if relationship_prop.uselist:
226+
if not getattr(strategies, 'SelectInLoader', None) or relationship_prop.uselist:
226227
# TODO Batch many-to-many and one-to-many relationships
227228
return _get_attr_resolver(obj_type, model_attr, model_attr)
228229

@@ -239,14 +240,18 @@ def batch_load_fn(self, parents): # pylint: disable=method-hidden
239240
than re-implementing and maintainnig a big chunk of the `selectin`
240241
loader logic ourselves.
241242
242-
The approach is to here to build a regular query that
243+
The approach here is to build a regular query that
243244
selects the parent and `selectin` load the relationship.
244245
But instead of having the query emits 2 `SELECT` statements
245246
when callling `all()`, we skip the first `SELECT` statement
246247
and jump right before the `selectin` loader is called.
247248
To accomplish this, we have to construct objects that are
248249
normally built in the first part of the query in order
249250
to call directly `SelectInLoader._load_for_path`.
251+
252+
TODO Move this logic to a util in the SQLAlchemy repo as per
253+
SQLAlchemy's main maitainer suggestion.
254+
See https://git.io/JewQ7
250255
"""
251256
session = Session.object_session(parents[0])
252257

@@ -258,7 +263,7 @@ def batch_load_fn(self, parents): # pylint: disable=method-hidden
258263
# The behavior of `selectin` is undefined if the parent is dirty
259264
assert parent not in session.dirty
260265

261-
loader = SelectInLoader(relationship_prop, (('lazy', 'selectin'),))
266+
loader = strategies.SelectInLoader(relationship_prop, (('lazy', 'selectin'),))
262267

263268
# Should the boolean be set to False? Does it matter for our purposes?
264269
states = [(sqlalchemy.inspect(parent), True) for parent in parents]

Diff for: setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ max-line-length = 120
99
no_lines_before=FIRSTPARTY
1010
known_graphene=graphene,graphql_relay,flask_graphql,graphql_server,sphinx_graphene_theme
1111
known_first_party=graphene_sqlalchemy
12-
known_third_party=app,database,flask,mock,models,nameko,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
12+
known_third_party=app,database,flask,mock,models,nameko,pkg_resources,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
1313
sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER
1414
skip_glob=examples/nameko_sqlalchemy
1515

0 commit comments

Comments
 (0)