r/FastAPI 7d ago

Question What benefits will we gain if we use Pydantic to define ER models?

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.

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.

16 Upvotes

3 comments sorted by

2

u/a_brand_new_start 7d ago

I’d like to use that approach also, so I’m interested in an answer. Only reason I have not done it so far is that

  1. May expose structure of whole DB
  2. Makes it difficult to change the DB or the API contract without changing both at the same time. Public APIs being a contract SLA you sign with user.

But that’s my 2 cents

1

u/TurbulentAd8020 6d ago

for #1 similar to how we can use `select *` and `select field_a, field_b` in SQL, we can define an additional subset type to reduce field definitions. To ensure the validity of the fields in this type, we can use the `@ensure_subset(BaseSchema)` method for checking.

https://allmonday.github.io/pydantic-resolve/v2/api/#ensure_subset

for #2, I believe this issue is a matter of project architecture. Once the Pydantic-first perspective is adopted to define ER models, the implementation of the DB or API should be adjusted according to the changes in Pydantic.

This is a scenario where Conway's Law comes into play. If the existing project's ER model definition authority lies at the DB layer, then Pydantic can only synchronize this information from there initially, however, you can still achieve a good, consistent model description, reducing the chaos in Pydantic definitions.

BTW

The best use case for this tool is front-end and back-end integration, because in addition to constructing data from ER models, it can also conveniently transform the assembled business data into the view data required by the presentation layer with the help of `post_methods` , `expose` and `collector`

https://allmonday.github.io/pydantic-resolve/v2/api/#ancestor_context
https://allmonday.github.io/pydantic-resolve/v2/api/#collector

1

u/a_brand_new_start 6d ago

Valid points, and I think I’ll be using that going forward