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.
This commit is contained in:
Campbell Alden 2026-08-21 00:24:14 +09:00
parent 63e51fa017
commit 86b0d88874
4 changed files with 82 additions and 11 deletions

View file

@ -1,16 +1,25 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from sqlalchemy import String, Text, ForeignKey, Engine, Interval, UniqueConstraint 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.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): class PublicationEntryModel(Base):
__tablename__ = 'publication_entry' __tablename__ = 'publication_entry'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(256))
text: Mapped[str] = mapped_column(Text()) text: Mapped[str] = mapped_column(Text())
sequences: Mapped[list['PublicationSequenceModel']] = relationship(back_populates='entry') sequences: Mapped[list['PublicationSequenceModel']] = relationship(back_populates='entry')
@ -55,13 +64,13 @@ class PublicationModel(Base):
def model_to_entry(db_pub_entry: PublicationEntryModel) -> Entry: 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: def model_to_sequence(db_pub_position: PublicationSequenceModel) -> Sequence:
return Sequence( return Sequence(
position=db_pub_position.position, 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, 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: def model_to_publication(db_pub: PublicationModel) -> Publication:
return Publication( return Publication(
id=db_pub.id, id=db_pub.id,
title=db_pub.title, title=db_pub.title,
description=db_pub.description, description=db_pub.description,
by_line=db_pub.by_line, 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: def get_publication_by_id(self, pub_id) -> Publication | None:
with Session(self._db) as session: with Session(self._db) as session:
publication = session.get(PublicationModel, pub_id) publication = session.get(PublicationModel, pub_id, options=[joinedload(PublicationModel.orders)])
if publication: if publication:
return model_to_publication(publication) return model_to_publication(publication)
@ -111,3 +127,22 @@ class PublicationRepoImpl(PublicationRepo):
def get_all_publications(self) -> list[PublicationProfile]: def get_all_publications(self) -> list[PublicationProfile]:
with Session(self._db) as session: with Session(self._db) as session:
return list(map(model_to_publication_profile, session.query(PublicationModel).all())) 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)

View file

@ -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 .service import PublicationService
from .repo import PublicationRepo from .repo import PublicationRepo
__all__ = ['Publication', 'PublicationProfile', 'Order', 'Sequence', 'Entry', 'PublicationService', 'PublicationRepo'] __all__ = [
'Publication',
'PublicationProfile',
'Order',
'OrderProfile',
'Sequence',
'Entry',
'PublicationService',
'PublicationRepo',
]

View file

@ -2,25 +2,44 @@ from datetime import timedelta
from dataclasses import dataclass, field from dataclasses import dataclass, field
@dataclass
class EntryProfile:
id: int
title: str
@dataclass @dataclass
class Entry: class Entry:
id: int id: int
title: str
text: str text: str
def to_profile(self) -> EntryProfile:
return EntryProfile(self.id, self.title)
@dataclass @dataclass
class Sequence: class Sequence:
position: int position: int
entry: Entry entry: EntryProfile
duration: timedelta | None duration: timedelta | None
@dataclass
class OrderProfile:
id: int
title: str
@dataclass @dataclass
class Order: class Order:
id: int id: int
title: str title: str
sequence_entries: list[Sequence] sequence_entries: list[Sequence]
def to_profile(self) -> OrderProfile:
return OrderProfile(id=self.id, title=self.title)
@dataclass @dataclass
class PublicationProfile: class PublicationProfile:
@ -35,7 +54,7 @@ class Publication:
title: str title: str
description: str description: str
by_line: 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 # Rather than using a subclass I think this makes sense
def to_profile(self) -> PublicationProfile: def to_profile(self) -> PublicationProfile:

View file

@ -1,6 +1,6 @@
import abc import abc
from .data import Publication, PublicationProfile from .data import Publication, PublicationProfile, Order, Entry
class PublicationRepo(abc.ABC): class PublicationRepo(abc.ABC):
@ -15,3 +15,11 @@ class PublicationRepo(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def get_all_publications(self) -> list[PublicationProfile]: def get_all_publications(self) -> list[PublicationProfile]:
pass 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