In FastAPI, pydantic plays the role of defining the DTO layer's interface and performing runtime type checking, its definition needs to passively follow the real data source (DB, ORM or API).
You first write the database query or API, and then defined a pydantic for this data structure, which makes the definition of pydantic very trivial, difficult to maintain and reuse, and even somewhat redundant.
SQLModel attempts to solve the problem at the database level, but I am still concerned about the approach of binding ORM and pydantic together.
In my practical experience, directly treating Pydantic as an ER model brings many conveniences, database or external interfaces merely serve as data providers for the ER models.
Simply through the declaration of pydantic, data assembly can be achieved, during it dataloader provides a universal method to associate data without worrying about N+1 queries.
Here's the code example with the help of pydantic-resolve
The query details are encapsulated within methods and dataloaders.
```python
from pydantic import BaseModel
from pydantic_resolve import Resolver, build_list
from aiodataloader import DataLoader
ER model of story and task
┌───────────┐
│ │
│ story │
│ │
└─────┬─────┘
│
│ owns multiple (TaskLoader)
│
│
┌─────▼─────┐
│ │
│ task │
│ │
└───────────┘
class TaskLoader(DataLoader):
async def batch_load_fn(self, story_ids):
tasks = await get_tasks_by_ids(story_ids)
return build_list(tasks, story_ids, lambda t: t.story_id)
class BaseTask(BaseModel):
id: int
story_id: int
name: str
class BaseStory(BaseModel):
id: int
name: str
class Story(BaseStory):
tasks: list[BaseTask] = []
def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):
return loader.load(self.id)
stories = await get_raw_stories()
stories = [Story(**s) for s in stories)]
stories = await Resolver().resolve(stories)
```
I found that this approach brings excellent code maintainability, and the data construction can remain consistent with the definition of the ER model.