DTOs¶
What are DTOs?¶
DTO stands for "Data Transfer Object". They are the filter through which data is accepted into, and output from the application.
DTO Factory¶
starlite-saqlalchemy includes
dto.factory()
which automatically creates pydantic models from
SQLAlchemy 2.0 ORM models.
Creating a SQLAlchemy ORM model¶
If you are new to SQLAlchemy, I cannot recommend their docs enough. Start at the beginning, and follow along until you are comfortable. I won't even try to compete with the quality and depth of information that can be found there - a credit to everyone who has contributed to that project over the years.
Configuring generated DTOs¶
dto.Purpose¶
The dto.Purpose enum tells the factory if the purpose of the DTO is to parse data submitted by the
client for updating or "writing" to a resource, or if it is to serialize data to be transmitted back
to, or "read" by the client.
The DTO objects generated by the factory may differ due to the intended purpose of the DTO. Here's an example of setting the DTO purpose:
from starlite_saqlalchemy import dto
from domain.users import User
ReadDTO = dto.factory("UserReadDTO", model=User, purpose=dto.Purpose.READ)
WriteDTO = dto.factory("UserWriteDTO", model=User, purpose=dto.Purpose.WRITE)
dto.Attrib¶
The dto.Attrib object is a container for configuring how the generated DTO object should reflect
the SQLAlchemy model field.
Define this in the SQLAlchemy info parameter to mapped_column(), for example,
mapped_column(info={"dto": dto.Attrib()}).
The DTO Attrib object has two values that can be set:
dto.Attrib.mark: a value of the enumdto.Mark.dto.Attrib.pydantic_field: return value of the pydanticFieldfunction that will be used to construct the pydantic model field for the model attribute.
dto.Mark¶
We use the info parameter to mapped_column() to guide dto.factory().
The dto.Mark enumeration is used to indicate on the whether properties on the SQLAlchemy model should always be private, or read-only.
For example:
from datetime import datetime
from sqlalchemy.orm import mapped_column
from starlite_saqlalchemy import dto, orm
class User(orm.Base):
name: str
password_hash: str = mapped_column(info={"dto": dto.Attrib(mark=dto.Mark.SKIP)})
updated_at: datetime = mapped_column(
info={"dto": dto.Attrib(mark=dto.Mark.READ_ONLY)}
)
ReadDTO = dto.factory("UserReadDTO", model=User, purpose=dto.Purpose.READ)
WriteDTO = dto.factory("UserWriteDTO", model=User, purpose=dto.Purpose.WRITE)
Both ReadDTO and WriteDTO are pydantic models that have a name attribute.
Neither ReadDTO or WriteDTO have a password_hash attribute - this is the side effect of
marking the column with dto.Mark.SKIP. Skipped columns are never included in any generated DTO
model, meaning that they are unable to be read or modified by the client.
ReadDTO has an updated_at field, while WriteDTO does not. This is the side effect of marking
the column with dto.Mark.READ_ONLY - these fields will only be included in DTOs generated for
dto.Purpose.READ and make sense for fields that have internally generated values.
The following class is pretty much the same as
orm.Base - the bundled
SQLAlchemy base class that comes with starlite-saqlalchemy.
from datetime import datetime
from uuid import UUID, uuid4
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from starlite_saqlalchemy import dto
class Base(DeclarativeBase):
id: Mapped[UUID] = mapped_column(
default=uuid4,
primary_key=True,
info={"dto": dto.Attrib(mark=dto.Mark.READ_ONLY)},
)
"""Primary key column."""
created: Mapped[datetime] = mapped_column(
default=datetime.now, info={"dto": dto.Attrib(mark=dto.Mark.READ_ONLY)}
)
"""Date/time of instance creation."""
updated: Mapped[datetime] = mapped_column(
default=datetime.now, info={"dto": dto.Attrib(mark=dto.Mark.READ_ONLY)}
)
Notice that all these fields are marked as dto.Mark.READ_ONLY. This means that they are unable to
be modified by clients, even if they include values for them in the payloads to POST/PUT/PATCH
routes.
You can inherit from orm.Base to create your SQLAlchemy models, but you don't have to. You can
choose to subclass orm.Base or roll your own base class altogether. dto.factory() will still
work as advertised.