Minimise database queries when resolving cards

This commit modifies the code which resolves the contents of cards
in boards and worklists such that instead of making a database
query per-card, two queries are made to obtain all tasks and stories
that there are cards for in the board/worklist.

The result of these queries is passed into the resolution function
in order to allow fast lookup of the underlying story/task for a card.

This will lead to much faster loading times for boards containing
large numbers of cards.

Change-Id: I189a1d9823a7d251af641cf24de52faac24584df
This commit is contained in:
Adam Coldrick 2017-03-12 00:28:25 +00:00
parent ca537c255a
commit 0c4f267433
3 changed files with 43 additions and 16 deletions

View File

@ -31,6 +31,8 @@ from storyboard.api.v1 import wmodels
from storyboard.common import decorators
from storyboard.common import exception as exc
from storyboard.db.api import boards as boards_api
from storyboard.db.api import stories as stories_api
from storyboard.db.api import tasks as tasks_api
from storyboard.db.api import timeline_events as events_api
from storyboard.db.api import users as users_api
from storyboard.db.api import worklists as worklists_api
@ -227,9 +229,13 @@ class BoardsController(rest.RestController):
board = boards_api.get(id)
user_id = request.current_user_id
story_cache = {story.id: story for story in stories_api.story_get_all(
board_id=id, current_user=user_id)}
task_cache = {task.id: task for task in tasks_api.task_get_all(
board_id=id, current_user=user_id)}
if boards_api.visible(board, user_id):
board_model = wmodels.Board.from_db_model(board)
board_model.resolve_lanes(board)
board_model.resolve_lanes(board, story_cache, task_cache)
board_model.resolve_due_dates(board)
board_model.resolve_permissions(board)
return board_model
@ -384,6 +390,11 @@ class BoardsController(rest.RestController):
if not boards_api.editable(original, user_id):
raise exc.NotFound(_("Board %s not found") % id)
story_cache = {story.id: story for story in stories_api.story_get_all(
board_id=id, current_user=user_id)}
task_cache = {task.id: task for task in tasks_api.task_get_all(
board_id=id, current_user=user_id)}
# We use copy here because we only need to check changes
# to the related objects, just the board's own attributes.
# Also, deepcopy trips up on the lanes' backrefs.
@ -402,7 +413,7 @@ class BoardsController(rest.RestController):
if boards_api.visible(updated_board, user_id):
board_model = wmodels.Board.from_db_model(updated_board)
board_model.resolve_lanes(updated_board)
board_model.resolve_lanes(updated_board, story_cache, task_cache)
board_model.resolve_permissions(updated_board)
return board_model
else:

View File

@ -717,6 +717,9 @@ class WorklistItem(base.APIBase):
@nodoc
def resolve_due_date(self, worklist_item):
if not worklist_item.display_due_date:
self.resolved_due_date = None
return
due_date = due_dates_api.get(worklist_item.display_due_date)
resolved = None
if due_dates_api.visible(due_date, request.current_user_id):
@ -724,10 +727,10 @@ class WorklistItem(base.APIBase):
self.resolved_due_date = resolved
@nodoc
def resolve_item(self, item):
def resolve_item(self, item, story_cache, task_cache):
user_id = request.current_user_id
if item.item_type == 'story':
story = stories_api.story_get(
story = story_cache.get(item.item_id) or stories_api.story_get(
item.item_id, current_user=request.current_user_id)
if story is None:
return False
@ -736,7 +739,7 @@ class WorklistItem(base.APIBase):
if due_dates_api.visible(date, user_id)]
self.story.due_dates = due_dates
elif item.item_type == 'task':
task = tasks_api.task_get(
task = task_cache.get(item.item_id) or tasks_api.task_get(
item.item_id, current_user=request.current_user_id)
if task is None or task.story is None:
return False
@ -799,20 +802,20 @@ class Worklist(base.APIBase):
items=[])
@nodoc
def resolve_items(self, worklist):
def resolve_items(self, worklist, story_cache={}, task_cache={}):
"""Resolve the contents of this worklist."""
self.items = []
user_id = request.current_user_id
if worklist.automatic:
self._resolve_automatic_items(worklist, user_id)
else:
self._resolve_set_items(worklist, user_id)
self._resolve_set_items(worklist, user_id, story_cache, task_cache)
@nodoc
def _resolve_automatic_items(self, worklist, user_id):
for item in worklists_api.filter_items(worklist):
item_model = WorklistItem(**item)
valid = item_model.resolve_item(item_model)
valid = item_model.resolve_item(item_model, {}, {})
if not valid:
continue
item_model.resolve_due_date(item_model)
@ -820,12 +823,12 @@ class Worklist(base.APIBase):
self.items.sort(key=lambda x: x.list_position)
@nodoc
def _resolve_set_items(self, worklist, user_id):
def _resolve_set_items(self, worklist, user_id, story_cache, task_cache):
for item in worklist.items:
if item.archived:
continue
item_model = WorklistItem.from_db_model(item)
valid = item_model.resolve_item(item)
valid = item_model.resolve_item(item, story_cache, task_cache)
if not valid:
continue
item_model.resolve_due_date(item)
@ -881,13 +884,14 @@ class Lane(base.APIBase):
items=[]))
@nodoc
def resolve_list(self, lane, resolve_items=True):
def resolve_list(self, lane, story_cache, task_cache, resolve_items=True):
"""Resolve the worklist which represents the lane."""
self.worklist = Worklist.from_db_model(lane.worklist)
self.worklist.resolve_permissions(lane.worklist)
self.worklist.resolve_filters(lane.worklist)
if resolve_items:
self.worklist.resolve_items(lane.worklist)
self.worklist.resolve_items(
lane.worklist, story_cache, task_cache)
else:
items = worklists_api.get_visible_items(
lane.worklist, current_user=request.current_user_id)
@ -961,12 +965,14 @@ class Board(base.APIBase):
users=[])
@nodoc
def resolve_lanes(self, board, resolve_items=True):
def resolve_lanes(self, board, story_cache={}, task_cache={},
resolve_items=True):
"""Resolve the lanes of the board."""
self.lanes = []
for lane in board.lanes:
lane_model = Lane.from_db_model(lane)
lane_model.resolve_list(lane, resolve_items)
lane_model.resolve_list(
lane, story_cache, task_cache, resolve_items)
self.lanes.append(lane_model)
self.lanes.sort(key=lambda x: x.position)

View File

@ -600,9 +600,13 @@ class WorklistsController(rest.RestController):
worklist = worklists_api.get(worklist_id)
user_id = request.current_user_id
story_cache = {story.id: story for story in stories_api.story_get_all(
worklist_id=worklist_id, current_user=user_id)}
task_cache = {task.id: task for task in tasks_api.task_get_all(
worklist_id=worklist_id, current_user=user_id)}
if worklist and worklists_api.visible(worklist, user_id):
worklist_model = wmodels.Worklist.from_db_model(worklist)
worklist_model.resolve_items(worklist)
worklist_model.resolve_items(worklist, story_cache, task_cache)
worklist_model.resolve_permissions(worklist)
worklist_model.resolve_filters(worklist)
return worklist_model
@ -811,6 +815,11 @@ class WorklistsController(rest.RestController):
if not worklists_api.editable(worklists_api.get(id), user_id):
raise exc.NotFound(_("Worklist %s not found") % id)
story_cache = {story.id: story for story in stories_api.story_get_all(
worklist_id=id, current_user=user_id)}
task_cache = {task.id: task for task in tasks_api.task_get_all(
worklist_id=id, current_user=user_id)}
# We don't use this endpoint to update the worklist's contents
if worklist.items != wtypes.Unset:
del worklist.items
@ -827,7 +836,8 @@ class WorklistsController(rest.RestController):
post_timeline_events(original, updated_worklist)
if worklists_api.visible(updated_worklist, user_id):
worklist_model = wmodels.Worklist.from_db_model(updated_worklist)
worklist_model.resolve_items(updated_worklist)
worklist_model.resolve_items(
updated_worklist, story_cache, task_cache)
worklist_model.resolve_permissions(updated_worklist)
return worklist_model
else: