11"""The dataloader uses "select in loading" strategy to load related entities."""
2- from typing import Any
2+ from asyncio import get_event_loop
3+ from typing import Any , Dict
34
45import aiodataloader
56import sqlalchemy
1011 is_sqlalchemy_version_less_than )
1112
1213
14+ class RelationshipLoader (aiodataloader .DataLoader ):
15+ cache = False
16+
17+ def __init__ (self , relationship_prop , selectin_loader ):
18+ super ().__init__ ()
19+ self .relationship_prop = relationship_prop
20+ self .selectin_loader = selectin_loader
21+
22+ async def batch_load_fn (self , parents ):
23+ """
24+ Batch loads the relationships of all the parents as one SQL statement.
25+
26+ There is no way to do this out-of-the-box with SQLAlchemy but
27+ we can piggyback on some internal APIs of the `selectin`
28+ eager loading strategy. It's a bit hacky but it's preferable
29+ than re-implementing and maintainnig a big chunk of the `selectin`
30+ loader logic ourselves.
31+
32+ The approach here is to build a regular query that
33+ selects the parent and `selectin` load the relationship.
34+ But instead of having the query emits 2 `SELECT` statements
35+ when callling `all()`, we skip the first `SELECT` statement
36+ and jump right before the `selectin` loader is called.
37+ To accomplish this, we have to construct objects that are
38+ normally built in the first part of the query in order
39+ to call directly `SelectInLoader._load_for_path`.
40+
41+ TODO Move this logic to a util in the SQLAlchemy repo as per
42+ SQLAlchemy's main maitainer suggestion.
43+ See https://git.io/JewQ7
44+ """
45+ child_mapper = self .relationship_prop .mapper
46+ parent_mapper = self .relationship_prop .parent
47+ session = Session .object_session (parents [0 ])
48+
49+ # These issues are very unlikely to happen in practice...
50+ for parent in parents :
51+ # assert parent.__mapper__ is parent_mapper
52+ # All instances must share the same session
53+ assert session is Session .object_session (parent )
54+ # The behavior of `selectin` is undefined if the parent is dirty
55+ assert parent not in session .dirty
56+
57+ # Should the boolean be set to False? Does it matter for our purposes?
58+ states = [(sqlalchemy .inspect (parent ), True ) for parent in parents ]
59+
60+ # For our purposes, the query_context will only used to get the session
61+ query_context = None
62+ if is_sqlalchemy_version_less_than ('1.4' ):
63+ query_context = QueryContext (session .query (parent_mapper .entity ))
64+ else :
65+ parent_mapper_query = session .query (parent_mapper .entity )
66+ query_context = parent_mapper_query ._compile_context ()
67+
68+ if is_sqlalchemy_version_less_than ('1.4' ):
69+ self .selectin_loader ._load_for_path (
70+ query_context ,
71+ parent_mapper ._path_registry ,
72+ states ,
73+ None ,
74+ child_mapper ,
75+ )
76+ else :
77+ self .selectin_loader ._load_for_path (
78+ query_context ,
79+ parent_mapper ._path_registry ,
80+ states ,
81+ None ,
82+ child_mapper ,
83+ None ,
84+ )
85+ return [
86+ getattr (parent , self .relationship_prop .key ) for parent in parents
87+ ]
88+
89+
90+ # Cache this across `batch_load_fn` calls
91+ # This is so SQL string generation is cached under-the-hood via `bakery`
92+ # Caching the relationship loader for each relationship prop.
93+ RELATIONSHIP_LOADERS_CACHE : Dict [
94+ sqlalchemy .orm .relationships .RelationshipProperty , RelationshipLoader
95+ ] = {}
96+
97+
1398def get_data_loader_impl () -> Any : # pragma: no cover
1499 """Graphene >= 3.1.1 ships a copy of aiodataloader with minor fixes. To preserve backward-compatibility,
15100 aiodataloader is used in conjunction with older versions of graphene"""
@@ -25,80 +110,23 @@ def get_data_loader_impl() -> Any: # pragma: no cover
25110
26111
27112def get_batch_resolver (relationship_prop ):
28- # Cache this across `batch_load_fn` calls
29- # This is so SQL string generation is cached under-the-hood via `bakery`
30- selectin_loader = strategies .SelectInLoader (relationship_prop , (('lazy' , 'selectin' ),))
31-
32- class RelationshipLoader (aiodataloader .DataLoader ):
33- cache = False
34-
35- async def batch_load_fn (self , parents ):
36- """
37- Batch loads the relationships of all the parents as one SQL statement.
38-
39- There is no way to do this out-of-the-box with SQLAlchemy but
40- we can piggyback on some internal APIs of the `selectin`
41- eager loading strategy. It's a bit hacky but it's preferable
42- than re-implementing and maintainnig a big chunk of the `selectin`
43- loader logic ourselves.
44-
45- The approach here is to build a regular query that
46- selects the parent and `selectin` load the relationship.
47- But instead of having the query emits 2 `SELECT` statements
48- when callling `all()`, we skip the first `SELECT` statement
49- and jump right before the `selectin` loader is called.
50- To accomplish this, we have to construct objects that are
51- normally built in the first part of the query in order
52- to call directly `SelectInLoader._load_for_path`.
53-
54- TODO Move this logic to a util in the SQLAlchemy repo as per
55- SQLAlchemy's main maitainer suggestion.
56- See https://git.io/JewQ7
57- """
58- child_mapper = relationship_prop .mapper
59- parent_mapper = relationship_prop .parent
60- session = Session .object_session (parents [0 ])
61-
62- # These issues are very unlikely to happen in practice...
63- for parent in parents :
64- # assert parent.__mapper__ is parent_mapper
65- # All instances must share the same session
66- assert session is Session .object_session (parent )
67- # The behavior of `selectin` is undefined if the parent is dirty
68- assert parent not in session .dirty
69-
70- # Should the boolean be set to False? Does it matter for our purposes?
71- states = [(sqlalchemy .inspect (parent ), True ) for parent in parents ]
72-
73- # For our purposes, the query_context will only used to get the session
74- query_context = None
75- if is_sqlalchemy_version_less_than ('1.4' ):
76- query_context = QueryContext (session .query (parent_mapper .entity ))
77- else :
78- parent_mapper_query = session .query (parent_mapper .entity )
79- query_context = parent_mapper_query ._compile_context ()
80-
81- if is_sqlalchemy_version_less_than ('1.4' ):
82- selectin_loader ._load_for_path (
83- query_context ,
84- parent_mapper ._path_registry ,
85- states ,
86- None ,
87- child_mapper
88- )
89- else :
90- selectin_loader ._load_for_path (
91- query_context ,
92- parent_mapper ._path_registry ,
93- states ,
94- None ,
95- child_mapper ,
96- None
97- )
98-
99- return [getattr (parent , relationship_prop .key ) for parent in parents ]
100-
101- loader = RelationshipLoader ()
113+ """Get the resolve function for the given relationship."""
114+
115+ def _get_loader (relationship_prop ):
116+ """Retrieve the cached loader of the given relationship."""
117+ loader = RELATIONSHIP_LOADERS_CACHE .get (relationship_prop , None )
118+ if loader is None or loader .loop != get_event_loop ():
119+ selectin_loader = strategies .SelectInLoader (
120+ relationship_prop , (('lazy' , 'selectin' ),)
121+ )
122+ loader = RelationshipLoader (
123+ relationship_prop = relationship_prop ,
124+ selectin_loader = selectin_loader ,
125+ )
126+ RELATIONSHIP_LOADERS_CACHE [relationship_prop ] = loader
127+ return loader
128+
129+ loader = _get_loader (relationship_prop )
102130
103131 async def resolve (root , info , ** args ):
104132 return await loader .load (root )
0 commit comments