reduce repeat queries in GET /api/users

add eager loading of several relationships that are ~always used when the given objects are requested
add specific eager loading of spawners to the users query

- roles, groups (always needed to resolve permissions)
- APIToken.user, service
This commit is contained in:
Min RK
2023-06-27 09:25:33 +02:00
parent 715b8f3cee
commit f24fbc761f
2 changed files with 27 additions and 7 deletions

View File

@@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone
from async_generator import aclosing
from dateutil.parser import parse as parse_date
from sqlalchemy import func, or_
from sqlalchemy.orm import raiseload, selectinload
from tornado import web
from tornado.iostream import StreamClosedError
@@ -90,6 +91,10 @@ class UserListAPIHandler(APIHandler):
# post_filter
post_filter = None
# starting query
# fetch users and groups, which will be used for filters
query = self.db.query(orm.User).outerjoin(orm.Group, orm.User.groups)
if state_filter in {"active", "ready"}:
# only get users with active servers
# an 'active' Spawner has a server record in the database
@@ -97,9 +102,9 @@ class UserListAPIHandler(APIHandler):
# it may still be in a pending start/stop state.
# join filters out users with no Spawners
query = (
self.db.query(orm.User)
query
# join filters out any Users with no Spawners
.join(orm.Spawner)
.join(orm.Spawner, orm.User._orm_spawners)
# this implicitly gets Users with *any* active server
.filter(orm.Spawner.server != None)
)
@@ -114,9 +119,8 @@ class UserListAPIHandler(APIHandler):
# this is the complement to the above query.
# how expensive is this with lots of servers?
query = (
self.db.query(orm.User)
.outerjoin(orm.Spawner)
.outerjoin(orm.Server)
query.outerjoin(orm.Spawner, orm.User._orm_spawners)
.outerjoin(orm.Server, orm.Spawner.server)
.group_by(orm.User.id)
.having(func.count(orm.Server.id) == 0)
)
@@ -124,7 +128,19 @@ class UserListAPIHandler(APIHandler):
raise web.HTTPError(400, "Unrecognized state filter: %r" % state_filter)
else:
# no filter, return all users
query = self.db.query(orm.User)
query = query.outerjoin(orm.Spawner, orm.User._orm_spawners).outerjoin(
orm.Server, orm.Spawner.server
)
# apply joinedload options
query = query.options(
selectinload(orm.User.groups),
selectinload(orm.User._orm_spawners),
)
# if testing, add raiseload to prevent lazy loading of anything we didn't ask for
if True:
# FIXME: detect tests
query = query.options(raiseload("*"))
sub_scope = self.parsed_scopes['list:users']
if sub_scope != scopes.Scope.ALL:

View File

@@ -263,7 +263,10 @@ class User(Base):
name = Column(Unicode(255), unique=True)
roles = relationship(
'Role', secondary='user_role_map', back_populates='users', lazy="selectin"
'Role',
secondary='user_role_map',
back_populates='users',
lazy="selectin",
)
_orm_spawners = relationship(
@@ -285,6 +288,7 @@ class User(Base):
"Group",
secondary='user_group_map',
back_populates="users",
lazy="selectin",
)
oauth_codes = relationship(
"OAuthCode", back_populates="user", cascade="all, delete-orphan"