felicity-lims/felicity/database/async_mixins/eagerload.py
2023-04-07 17:52:19 +02:00

137 lines
4.1 KiB
Python

try:
pass
except ImportError: # pragma: no cover
pass
from sqlalchemy.future import select
from sqlalchemy.orm import joinedload, subqueryload
from sqlalchemy.orm.attributes import InstrumentedAttribute
from .session 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 StockProduct.user: {...},
# we transform, say, StockProduct.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 select(cls).options(
*eager_expr(schema or {})
) # 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 select(cls).options(*options) # 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 select(cls).options(*options) # cls.query.options(*options)