-
Notifications
You must be signed in to change notification settings - Fork 1
/
eager_load.py
130 lines (110 loc) · 3.84 KB
/
eager_load.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
try:
from typing import List
except ImportError:
pass
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import subqueryload
from sqlalchemy.orm.attributes import InstrumentedAttribute
from .session_mixin import SessionMixin
JOINED = 'joined'
SUBQUERY = 'subquery'
def eager_expr(schema):
"""
:type schema: dict
"""
flat_schema = _flatten_schema(schema)
return _eager_expr_from_flat_schema(flat_schema)
def _flatten_schema(schema):
"""
:type schema: dict
"""
def _flatten(schema, parent_path, result):
"""
:type schema: dict
"""
for path, value in schema.items():
# for supporting schemas like Product.user: {...},
# we transform, say, Product.user to 'user' string
if isinstance(path, InstrumentedAttribute):
path = path.key
if isinstance(value, tuple):
join_method, inner_schema = value[0], value[1]
elif isinstance(value, dict):
join_method, inner_schema = JOINED, value
else:
join_method, inner_schema = value, None
full_path = parent_path + '.' + path if parent_path else path
result[full_path] = join_method
if inner_schema:
_flatten(inner_schema, full_path, result)
result = {}
_flatten(schema, '', result)
return result
def _eager_expr_from_flat_schema(flat_schema):
"""
:type flat_schema: dict
"""
result = []
for path, join_method in flat_schema.items():
if join_method == JOINED:
result.append(joinedload(path))
elif join_method == SUBQUERY:
result.append(subqueryload(path))
else:
raise ValueError('Bad join method `{}` in `{}`'
.format(join_method, path))
return result
class EagerLoadMixin(SessionMixin):
__abstract__ = True
@classmethod
def with_(cls, schema):
"""
Query class and eager load schema at once.
:type schema: dict
Example:
schema = {
'user': JOINED, # joinedload user
'comments': (SUBQUERY, { # load comments in separate query
'user': JOINED # but, in this separate query, join user
})
}
# the same schema using class properties:
schema = {
Post.user: JOINED,
Post.comments: (SUBQUERY, {
Comment.user: JOINED
})
}
User.with_(schema).first()
"""
return cls.query.options(*eager_expr(schema or {}))
@classmethod
def with_joined(cls, *paths):
"""
Eagerload for simple cases where we need to just
joined load some relations
In strings syntax, you can split relations with dot
due to this SQLAlchemy feature: https://goo.gl/yM2DLX
:type paths: *List[str] | *List[InstrumentedAttribute]
Example 1:
Comment.with_joined('user', 'post', 'post.comments').first()
Example 2:
Comment.with_joined(Comment.user, Comment.post).first()
"""
options = [joinedload(path) for path in paths]
return cls.query.options(*options)
@classmethod
def with_subquery(cls, *paths):
"""
Eagerload for simple cases where we need to just
joined load some relations
In strings syntax, you can split relations with dot
(it's SQLAlchemy feature)
:type paths: *List[str] | *List[InstrumentedAttribute]
Example 1:
User.with_subquery('posts', 'posts.comments').all()
Example 2:
User.with_subquery(User.posts, User.comments).all()
"""
options = [subqueryload(path) for path in paths]
return cls.query.options(*options)