Python API Development with FastAPI
Advanced FastAPI
Sqlalchemy Relationships
When designing a social media–type application, it's common to retrieve posts along with details about the post creator. Instead of returning just an owner ID— which holds little meaning for end users— you can include user information such as username or email. SQLAlchemy relationships enable the automatic fetching of this related user data when querying posts.
For example, consider the following JSON response when fetching posts:
[
{
"title": "asdfsdf",
"content": "this is the new content",
"published": true,
"id": 4,
"created_at": "2021-08-28T21:18:13.460585-04:00",
"owner_id": 21
},
{
"title": "this is the new title wahoo",
"content": "this is the new content",
"published": true,
"id": 9,
"created_at": "2021-08-28T21:48:30.323527-04:00",
"owner_id": 3
}
]
Without a relationship, you would need to execute a separate query for each post, fetching the user details associated with the owner ID. With SQLAlchemy's relationship feature, the ORM automatically performs the necessary join to include the corresponding user details.
How It Works
This setup does not add a foreign key constraint in the database by itself; it simply instructs SQLAlchemy to retrieve the related user based on the owner_id
when querying the posts.
Setting Up the Models
Below is an example of how you can configure your models with a relationship between posts and users.
from sqlalchemy import Column, Integer, String, Boolean, TIMESTAMP, ForeignKey, text
from sqlalchemy.orm import relationship
from .database import Base
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, nullable=False)
title = Column(String, nullable=False)
content = Column(String, nullable=False)
published = Column(Boolean, server_default='TRUE', nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()'))
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
# Automatically fetch the User based on owner_id.
owner = relationship("User")
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, nullable=False)
email = Column(String, nullable=False, unique=True)
password = Column(String, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()'))
With this setup, when you retrieve a post, SQLAlchemy will automatically fetch the corresponding user and attach it as the owner
property.
Updating the Pydantic Schemas
Even though SQLAlchemy fetches the related user automatically, your JSON responses may still only display the owner_id
. To include the complete user information, update your Pydantic schemas to include an owner
field.
from datetime import datetime
from pydantic import BaseModel, EmailStr
class PostBase(BaseModel):
title: str
content: str
published: bool
class UserOut(BaseModel):
id: int
email: EmailStr
created_at: datetime
class Config:
orm_mode = True
class Post(PostBase):
id: int
created_at: datetime
owner_id: int
owner: UserOut # Now returns detailed user information
class Config:
orm_mode = True
class UserCreate(BaseModel):
email: EmailStr
password: str
Important
Ensure that the UserOut
class is defined before the Post
schema to avoid any errors due to the order of declaration.
After these changes, the posts endpoint will provide responses that include user details such as user ID, email, and account creation date. This enhancement eliminates the need for an extra query to fetch user details on the client side.
Application Logging
When you test these changes and start your server, you should see log output similar to the following:
INFO: 127.0.0.1:57933 - "GET /posts/ HTTP/1.1" 200 OK
Once the updated relationships and schemas are in place, the posts endpoint might produce logs like these:
INFO: 127.0.0.1:57933 - "GET /posts HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:57933 - "GET /posts HTTP/1.1" 200 OK
INFO: 127.0.0.1:55079 - "GET /posts/10 HTTP/1.1" 200 OK
Final Model Configuration
Below is the final configuration for the Post model, which includes the relationship setup:
from sqlalchemy import Column, Integer, String, Boolean, TIMESTAMP, ForeignKey, text
from sqlalchemy.orm import relationship
from .database import Base
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, nullable=False)
title = Column(String, nullable=False)
content = Column(String, nullable=False)
published = Column(Boolean, server_default='TRUE', nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()'))
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
owner = relationship("User")
When you start your server, you can confirm that the relationship is working as expected with logs similar to the following:
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Database connection was successful
INFO: Started server process [5996]
INFO: 127.0.0.1:62004 - "GET /posts HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:62004 - "GET /posts HTTP/1.1" 200 OK
INFO: 127.0.0.1:55079 - "GET /posts/10 HTTP/1.1" 200 OK
Summary
By defining a relationship in your SQLAlchemy model and updating your Pydantic schemas, you can streamline your application's data handling by automatically including comprehensive user information with each post. This approach not only simplifies client-side operations but also improves the overall efficiency of your API responses.
For more details on SQLAlchemy relationships, check out the SQLAlchemy documentation.
Watch Video
Watch video content