1
1
"""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
3
4
4
5
import aiodataloader
5
6
import sqlalchemy
10
11
is_sqlalchemy_version_less_than )
11
12
12
13
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
+
13
98
def get_data_loader_impl () -> Any : # pragma: no cover
14
99
"""Graphene >= 3.1.1 ships a copy of aiodataloader with minor fixes. To preserve backward-compatibility,
15
100
aiodataloader is used in conjunction with older versions of graphene"""
@@ -25,80 +110,23 @@ def get_data_loader_impl() -> Any: # pragma: no cover
25
110
26
111
27
112
def 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 )
102
130
103
131
async def resolve (root , info , ** args ):
104
132
return await loader .load (root )
0 commit comments