perf(campaign): batch tenant summaries

This commit is contained in:
2026-07-30 01:15:38 +02:00
parent 961d5d1130
commit 46df12c025
2 changed files with 98 additions and 0 deletions

View File

@@ -161,6 +161,26 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
return {"campaigns": session.query(Campaign).filter(Campaign.tenant_id == tenant_id).count()}
def _tenant_summary_batch(session, tenant_ids) -> dict[str, dict[str, int]]:
from sqlalchemy import func
from govoplan_campaign.backend.db.models import Campaign
ids = tuple(dict.fromkeys(str(tenant_id) for tenant_id in tenant_ids if tenant_id))
if not ids:
return {}
rows = (
session.query(Campaign.tenant_id, func.count(Campaign.id))
.filter(Campaign.tenant_id.in_(ids))
.group_by(Campaign.tenant_id)
.all()
)
return {
tenant_id: {"campaigns": int(count)}
for tenant_id, count in rows
}
def _campaigns_router(context: ModuleContext):
from govoplan_campaign.backend.runtime import configure_runtime
@@ -236,6 +256,7 @@ manifest = ModuleManifest(
route_factory=_campaigns_router,
role_templates=ROLE_TEMPLATES,
tenant_summary_providers=(_tenant_summary,),
tenant_summary_batch_providers=(_tenant_summary_batch,),
nav_items=(
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=CAMPAIGN_MODULE_REQUIRED_ANY, order=20),
),

View File

@@ -0,0 +1,77 @@
from __future__ import annotations
import unittest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session
from govoplan_access.backend.db.models import Account, Group, User
from govoplan_campaign.backend.db.models import Campaign
from govoplan_campaign.backend.manifest import _tenant_summary_batch
from govoplan_core.core.change_sequence import ChangeSequenceEntry
from govoplan_core.db.base import Base
class CampaignTenantSummaryBatchTests(unittest.TestCase):
def test_batch_summary_groups_all_tenants_in_one_query(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(
engine,
tables=[
Account.__table__,
User.__table__,
Group.__table__,
Campaign.__table__,
ChangeSequenceEntry.__table__,
],
)
try:
with Session(engine) as session:
session.add_all(
[
Campaign(
id="campaign-1",
tenant_id="tenant-1",
external_id="one",
name="One",
),
Campaign(
id="campaign-2",
tenant_id="tenant-1",
external_id="two",
name="Two",
),
Campaign(
id="campaign-3",
tenant_id="tenant-2",
external_id="three",
name="Three",
),
]
)
session.commit()
query_count = 0
def count_query(*_args: object) -> None:
nonlocal query_count
query_count += 1
event.listen(engine, "before_cursor_execute", count_query)
try:
counts = _tenant_summary_batch(
session,
["tenant-1", "tenant-2", "tenant-empty"],
)
finally:
event.remove(engine, "before_cursor_execute", count_query)
self.assertEqual(1, query_count)
self.assertEqual({"campaigns": 2}, counts["tenant-1"])
self.assertEqual({"campaigns": 1}, counts["tenant-2"])
self.assertNotIn("tenant-empty", counts)
finally:
engine.dispose()
if __name__ == "__main__":
unittest.main()