From eeabf68e50dc3a0a440937d2ba8854db39d54bec Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:29:31 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20optimize=20manual=20lending?= =?UTF-8?q?=20N+1=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimized the \`manual_lending\` route by replacing O(N) loop-based database lookups with efficient MongoDB aggregation pipelines. This reduces the number of database roundtrips from O(N) to O(1), significantly improving page load performance when many active lendings or recent consumable usages exist. - Replaced active tool lending loop with aggregation using \$lookup and \$project. - Replaced recent consumable usage loop with aggregation using \$lookup and \$project. - Maintained existing sorting logic and data structure for full compatibility. Co-authored-by: Woschj <81321922+Woschj@users.noreply.github.com> --- .jules/bolt.md | 3 + app/routes/admin/system.py | 111 +++++++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 1de7202..bb9c3e9 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -8,3 +8,6 @@ ## 2025-07-14 - [Aggregation for Activity Feeds] **Learning:** Replacing loop-based lookups (N+1 queries) with single aggregation pipelines in `get_recent_activity` reduces database roundtrips by over 90% (from 42 queries to 2). Using `$unwind` without `preserveNullAndEmptyArrays` effectively replicates `if tool and worker:` filtering logic in the database layer. **Action:** Apply similar aggregation patterns to other feed-like features (e.g., `manual_lending` in `app/routes/admin/system.py`). +## 2025-01-24 - [N+1 Query Resolution in Manual Lending] +**Learning:** The `manual_lending` route had a severe N+1 query problem where each active lending and recent consumable usage triggered two additional `find_one` calls. Replacing these with separate aggregation pipelines using `$lookup` and `$project` reduces the query count from O(N) to O(1). +**Action:** Always check loop-based lookups in routes that list multiple items and prioritize aggregation even if local `mongomock` tests don't show immediate performance gains, as the real benefit is in reducing database roundtrips. diff --git a/app/routes/admin/system.py b/app/routes/admin/system.py index ed44480..1104b25 100644 --- a/app/routes/admin/system.py +++ b/app/routes/admin/system.py @@ -124,44 +124,83 @@ def manual_lending(): # Hole aktuelle Ausleihen current_lendings = [] - # Aktuelle Werkzeug-Ausleihen - active_tool_lendings = mongodb.find('lendings', {'returned_at': None}) - for lending in active_tool_lendings: - tool = mongodb.find_one('tools', {'barcode': lending['tool_barcode']}) - worker = mongodb.find_one('workers', {'barcode': lending['worker_barcode']}) - - if tool and worker: - current_lendings.append({ - 'item_name': tool['name'], - 'item_barcode': tool['barcode'], - 'worker_name': f"{worker['firstname']} {worker['lastname']}", - 'worker_barcode': worker['barcode'], - 'action_date': lending['lent_at'], - 'category': 'Werkzeug', - 'amount': None - }) - - # Aktuelle Verbrauchsmaterial-Ausgaben (letzte 30 Tage) + # Aktuelle Werkzeug-Ausleihen (Optimiert mit Aggregation zur Vermeidung von N+1 Problemen) + active_tool_lendings_pipeline = [ + {'$match': {'returned_at': None}}, + { + '$lookup': { + 'from': 'tools', + 'localField': 'tool_barcode', + 'foreignField': 'barcode', + 'as': 'tool_info' + } + }, + {'$unwind': '$tool_info'}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker_info' + } + }, + {'$unwind': '$worker_info'}, + { + '$project': { + 'item_name': '$tool_info.name', + 'item_barcode': '$tool_info.barcode', + 'worker_name': {'$concat': ['$worker_info.firstname', ' ', '$worker_info.lastname']}, + 'worker_barcode': '$worker_info.barcode', + 'action_date': '$lent_at', + 'category': {'$literal': 'Werkzeug'}, + 'amount': {'$literal': None} + } + } + ] + + current_lendings.extend(list(mongodb.aggregate('lendings', active_tool_lendings_pipeline))) + + # Aktuelle Verbrauchsmaterial-Ausgaben der letzten 30 Tage (Optimiert mit Aggregation) thirty_days_ago = datetime.now() - timedelta(days=30) - recent_consumable_usages = mongodb.find('consumable_usages', { - 'used_at': {'$gte': thirty_days_ago}, - 'quantity': {'$lt': 0} # Nur Ausgaben (negative Werte), nicht Entnahmen - }) + recent_consumable_usages_pipeline = [ + { + '$match': { + 'used_at': {'$gte': thirty_days_ago}, + 'quantity': {'$lt': 0} # Nur Ausgaben (negative Werte) + } + }, + { + '$lookup': { + 'from': 'consumables', + 'localField': 'consumable_barcode', + 'foreignField': 'barcode', + 'as': 'consumable_info' + } + }, + {'$unwind': '$consumable_info'}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker_info' + } + }, + {'$unwind': '$worker_info'}, + { + '$project': { + 'item_name': '$consumable_info.name', + 'item_barcode': '$consumable_info.barcode', + 'worker_name': {'$concat': ['$worker_info.firstname', ' ', '$worker_info.lastname']}, + 'worker_barcode': '$worker_info.barcode', + 'action_date': '$used_at', + 'category': {'$literal': 'Verbrauchsmaterial'}, + 'amount': '$quantity' + } + } + ] - for usage in recent_consumable_usages: - consumable = mongodb.find_one('consumables', {'barcode': usage['consumable_barcode']}) - worker = mongodb.find_one('workers', {'barcode': usage['worker_barcode']}) - - if consumable and worker: - current_lendings.append({ - 'item_name': consumable['name'], - 'item_barcode': consumable['barcode'], - 'worker_name': f"{worker['firstname']} {worker['lastname']}", - 'worker_barcode': worker['barcode'], - 'action_date': usage['used_at'], - 'category': 'Verbrauchsmaterial', - 'amount': usage['quantity'] - }) + current_lendings.extend(list(mongodb.aggregate('consumable_usages', recent_consumable_usages_pipeline))) # Sortiere nach Datum (neueste zuerst) def safe_date_key(lending):