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 async_generator import aclosing
from dateutil.parser import parse as parse_date from dateutil.parser import parse as parse_date
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from sqlalchemy.orm import raiseload, selectinload
from tornado import web from tornado import web
from tornado.iostream import StreamClosedError from tornado.iostream import StreamClosedError
@@ -90,6 +91,10 @@ class UserListAPIHandler(APIHandler):
# post_filter # post_filter
post_filter = None 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"}: if state_filter in {"active", "ready"}:
# only get users with active servers # only get users with active servers
# an 'active' Spawner has a server record in the database # 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. # it may still be in a pending start/stop state.
# join filters out users with no Spawners # join filters out users with no Spawners
query = ( query = (
self.db.query(orm.User) query
# join filters out any Users with no Spawners # 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 # this implicitly gets Users with *any* active server
.filter(orm.Spawner.server != None) .filter(orm.Spawner.server != None)
) )
@@ -114,9 +119,8 @@ class UserListAPIHandler(APIHandler):
# this is the complement to the above query. # this is the complement to the above query.
# how expensive is this with lots of servers? # how expensive is this with lots of servers?
query = ( query = (
self.db.query(orm.User) query.outerjoin(orm.Spawner, orm.User._orm_spawners)
.outerjoin(orm.Spawner) .outerjoin(orm.Server, orm.Spawner.server)
.outerjoin(orm.Server)
.group_by(orm.User.id) .group_by(orm.User.id)
.having(func.count(orm.Server.id) == 0) .having(func.count(orm.Server.id) == 0)
) )
@@ -124,7 +128,19 @@ class UserListAPIHandler(APIHandler):
raise web.HTTPError(400, "Unrecognized state filter: %r" % state_filter) raise web.HTTPError(400, "Unrecognized state filter: %r" % state_filter)
else: else:
# no filter, return all users # 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'] sub_scope = self.parsed_scopes['list:users']
if sub_scope != scopes.Scope.ALL: if sub_scope != scopes.Scope.ALL:

View File

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