I follow the FastAPI Tutorial and am not quite sure what the exact relationship between the proposed data objects is.
We have the models.py
file:
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
items = relationship("Item", back_populates="owner")
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, index=True)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="items")
And the schemas.py
file:
from typing import List, Union
from pydantic import BaseModel
class ItemBase(BaseModel):
title: str
description: Union[str, None] = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
owner_id: int
class Config:
orm_mode = True
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
items: List[Item] = []
class Config:
orm_mode = True
Those classes are then used to define db queries like in the crud.py
file:
from sqlalchemy.orm import Session
from . import models, schemas
def get_user(db: Session, user_id: int):
return db.query(models.User).filter(models.User.id == user_id).first()
def get_user_by_email(db: Session, email: str):
return db.query(models.User).filter(models.User.email == email).first()
def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, user: schemas.UserCreate):
fake_hashed_password = user.password + "notreallyhashed"
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def get_items(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Item).offset(skip).limit(limit).all()
def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
db_item = models.Item(**item.dict(), owner_id=user_id)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
And in the FastAPI code main.py
:
from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from . import crud, models, schemas
from .database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)
@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = crud.get_users(db, skip=skip, limit=limit)
return users
@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
return crud.create_user_item(db=db, item=item, user_id=user_id)
@app.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
items = crud.get_items(db, skip=skip, limit=limit)
return items
From what I understand:
models
data classes define the SQL tables.schemas
data classes define the API that FastAPI uses to interact with the database.What I don't understand:
crud.create_user_item
I expected the return type to be schemas.Item
, since that return type is used by FastAPI again.@app.post("/users/{user_id}/items/", response_model=schemas.Item)
in the main.py
is wrong, or how can I understand the return type inconsistency?models.Item
, how is that handled by FastAPI?crud.get_user
?I'll go through your bullet points one by one.
The
models
data classes define the SQL tables.
Yes. More precisely, the orm classes that map to actual database tables are defined in the models
module.
The
schemas
data classes define the API that FastAPI uses to interact with the database.
Yes and no. The Pydantic models in the schemas
module define the data schemas relevant to the API, yes. But that has nothing to do with the database yet. Some of these schemas define what data is expected to be received by certain API endpoints for the request to be considered valid. Others define what the data returned by certain endpoints will look like.
They must be convertible into each other so that the set-up works.
While the database table schemas and the API data schemas are usually very similar, that is not necessarily the case. In the tutorial however, they correspond quite neatly, which allows succinct crud code, like this:
db_item = models.Item(**item.dict(), owner_id=user_id)
Here item
is a Pydantic model instance, i.e. one of your API data schemas schemas.ItemCreate
containing data you decided is necessary for creating a new item. Since its fields (their names and types) correspond to those of the database model models.Item
, the latter can be instantiated from the dictionary representation of the former (with the addition of the owner_id
).
In
crud.create_user_item
I expected the return type to beschemas.Item
, since that return type is used by FastAPI again.
No, this is exactly the magic of FastAPI. The function create_user_item
returns an instance of models.Item
, i.e. the ORM object as constructed from the database (after calling session.refresh
on it):
def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
db_item = models.Item(**item.dict(), owner_id=user_id)
...
return db_item
And the API route handler function create_item_for_user
actually does return that same object (of class models.Item
).
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
return crud.create_user_item(db=db, item=item, user_id=user_id)
However, the @app.post
decorator takes that object and uses it to construct an instance of the response_model
you defined for that route, which is schemas.Item
in this case. This is why you set orm_mode
in your schemas.Item
model:
class Config:
orm_mode = True
This allows an instance of that class to be created via the .from_orm
method. (This only applies to Pydantic v1 Models.) That all happens behind the scenes and again depends on the SQLAlchemy model corresponding to the Pydantic model with regards to field names and types. Otherwise validation fails.
According to my understanding the response model [...] is wrong
No, see above. The decorated route function actually returns an instance of the schemas.Item
model.
However inferring from the code, the actual return type must be
models.Item
Yes, see above. The return type of the undecorated route handler function create_item_for_user
is in fact models.Item
. But its return type is not the response model.
I assume that to reduce confusion the documentation example does not annotate the return type of those route functions. If it did, it would look like this:
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
) -> models.Item:
return crud.create_user_item(db=db, item=item, user_id=user_id)
It may help to remember that a function decorator is just syntactic sugar for a function that takes a function as argument and (usually) returns a function. Typically the returned function actually internally calls the function passed to it as argument and does additional things before and/or after that call. I could rewrite the route above like this and it would be exactly the same:
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
) -> models.Item:
return crud.create_user_item(db=db, item=item, user_id=user_id)
create_item_for_user = app.post(
"/users/{user_id}/items/", response_model=schemas.Item
)(create_item_for_user)
What would be the return type of
crud.get_user
?
That would be models.User
because that is the database model and is what the first
method of that query returns.
def get_user(db: Session, user_id: int) -> models.User:
return db.query(models.User).filter(models.User.id == user_id).first()
This is then again returned by the read_user
API route function in the same fashion as I explained above for models.Item
.
@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)) -> models.User:
db_user = crud.get_user(db, user_id=user_id)
...
return db_user # <-- instance of `models.User`
That is, the models.User
object is intercepted by the internal function of the decorator and (because of the defined response_model
) passed to schemas.User.from_orm
, which returns a schemas.User
object.
Hope this helps.