From 86b0d88874a443e55ffdba54a441ef6c1ca0d4eb Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Fri, 21 Aug 2026 00:24:14 +0900 Subject: [PATCH] Improve the publication table / data types to denormalize the values Returning profile values where necessary will remove the need to compute heavy joins unless the user actually wants all the data. Now Publication points at OrderProfiles which don't expose the sequences Orders->Sequence->EntryProfile avoids including the full `text` for the sequence. --- src/infra/publication.py | 47 +++++++++++++++++++++++---- src/services/publications/__init__.py | 13 ++++++-- src/services/publications/data.py | 23 +++++++++++-- src/services/publications/repo.py | 10 +++++- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/infra/publication.py b/src/infra/publication.py index b07df6d..fb28357 100644 --- a/src/infra/publication.py +++ b/src/infra/publication.py @@ -1,16 +1,25 @@ from datetime import timedelta import logging from sqlalchemy import String, Text, ForeignKey, Engine, Interval, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column, relationship, Session +from sqlalchemy.orm import Mapped, mapped_column, relationship, Session, joinedload from src.infra.db import Base -from src.services.publications import Publication, Order, Entry, Sequence, PublicationProfile, PublicationRepo +from src.services.publications import ( + Publication, + Order, + Entry, + Sequence, + PublicationProfile, + PublicationRepo, + OrderProfile, +) class PublicationEntryModel(Base): __tablename__ = 'publication_entry' id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(256)) text: Mapped[str] = mapped_column(Text()) sequences: Mapped[list['PublicationSequenceModel']] = relationship(back_populates='entry') @@ -55,13 +64,13 @@ class PublicationModel(Base): def model_to_entry(db_pub_entry: PublicationEntryModel) -> Entry: - return Entry(id=db_pub_entry.id, text=db_pub_entry.text) + return Entry(id=db_pub_entry.id, text=db_pub_entry.text, title=db_pub_entry.title) def model_to_sequence(db_pub_position: PublicationSequenceModel) -> Sequence: return Sequence( position=db_pub_position.position, - entry=model_to_entry(db_pub_position.entry), + entry=model_to_entry(db_pub_position.entry).to_profile(), duration=db_pub_position.duration, ) @@ -74,13 +83,20 @@ def model_to_order(db_pub_order: PublicationOrderModel) -> Order: ) +def model_to_order_profile(db_pub_order: PublicationOrderModel) -> OrderProfile: + return OrderProfile( + id=db_pub_order.id, + title=db_pub_order.title, + ) + + def model_to_publication(db_pub: PublicationModel) -> Publication: return Publication( id=db_pub.id, title=db_pub.title, description=db_pub.description, by_line=db_pub.by_line, - orders=list(map(model_to_order, db_pub.orders)), + orders=list(map(lambda m: model_to_order_profile(m), db_pub.orders)), ) @@ -96,7 +112,7 @@ class PublicationRepoImpl(PublicationRepo): def get_publication_by_id(self, pub_id) -> Publication | None: with Session(self._db) as session: - publication = session.get(PublicationModel, pub_id) + publication = session.get(PublicationModel, pub_id, options=[joinedload(PublicationModel.orders)]) if publication: return model_to_publication(publication) @@ -111,3 +127,22 @@ class PublicationRepoImpl(PublicationRepo): def get_all_publications(self) -> list[PublicationProfile]: with Session(self._db) as session: return list(map(model_to_publication_profile, session.query(PublicationModel).all())) + + def get_full_order(self, order_id: int) -> Order | None: + with Session(self._db) as session: + order = session.get( + PublicationOrderModel, order_id, options=[joinedload(PublicationOrderModel.sequence_entries)] + ) + + if order: + return model_to_order(order) + + def get_full_entry(self, entry_id: int) -> Entry | None: + with Session(self._db) as session: + order = session.get( + PublicationEntryModel, + entry_id, + ) + + if order: + return model_to_entry(order) diff --git a/src/services/publications/__init__.py b/src/services/publications/__init__.py index 451db29..b19e688 100644 --- a/src/services/publications/__init__.py +++ b/src/services/publications/__init__.py @@ -1,5 +1,14 @@ -from .data import Publication, PublicationProfile, Order, Sequence, Entry +from .data import Publication, PublicationProfile, Order, Sequence, Entry, OrderProfile from .service import PublicationService from .repo import PublicationRepo -__all__ = ['Publication', 'PublicationProfile', 'Order', 'Sequence', 'Entry', 'PublicationService', 'PublicationRepo'] +__all__ = [ + 'Publication', + 'PublicationProfile', + 'Order', + 'OrderProfile', + 'Sequence', + 'Entry', + 'PublicationService', + 'PublicationRepo', +] diff --git a/src/services/publications/data.py b/src/services/publications/data.py index 7d2bf59..c29a563 100644 --- a/src/services/publications/data.py +++ b/src/services/publications/data.py @@ -2,25 +2,44 @@ from datetime import timedelta from dataclasses import dataclass, field +@dataclass +class EntryProfile: + id: int + title: str + + @dataclass class Entry: id: int + title: str text: str + def to_profile(self) -> EntryProfile: + return EntryProfile(self.id, self.title) + @dataclass class Sequence: position: int - entry: Entry + entry: EntryProfile duration: timedelta | None +@dataclass +class OrderProfile: + id: int + title: str + + @dataclass class Order: id: int title: str sequence_entries: list[Sequence] + def to_profile(self) -> OrderProfile: + return OrderProfile(id=self.id, title=self.title) + @dataclass class PublicationProfile: @@ -35,7 +54,7 @@ class Publication: title: str description: str by_line: str - orders: list[Order] = field(default_factory=list) + orders: list[OrderProfile] = field(default_factory=list) # Rather than using a subclass I think this makes sense def to_profile(self) -> PublicationProfile: diff --git a/src/services/publications/repo.py b/src/services/publications/repo.py index 56502c6..0494fec 100644 --- a/src/services/publications/repo.py +++ b/src/services/publications/repo.py @@ -1,6 +1,6 @@ import abc -from .data import Publication, PublicationProfile +from .data import Publication, PublicationProfile, Order, Entry class PublicationRepo(abc.ABC): @@ -15,3 +15,11 @@ class PublicationRepo(abc.ABC): @abc.abstractmethod def get_all_publications(self) -> list[PublicationProfile]: pass + + @abc.abstractmethod + def get_full_order(self, order_id: int) -> Order | None: + pass + + @abc.abstractmethod + def get_full_entry(self, entry_id: int) -> Entry | None: + pass