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
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)

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 .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
@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:

View file

@ -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