Time to tackle one of the trickier parts of any CRUD API - updating existing records! Today we're building the update functionality for our blog posts. This is where things get a bit more interesting because we need to find the blog first, then modify it.
The CRUD Layer Update
Let's add our update function to database/crud/blog.py:
from typing import Optional
def update_blog_by_slug(slug: str, blog: CreateBlog, db: Session) -> Optional[Blog]:
statement = select(Blog).where(Blog.slug == slug)
result = db.exec(statement)
db_blog = result.first()
if not db_blog:
return None
db_blog.title = blog.title
db_blog.slug = blog.slug
db_blog.content = blog.content
db.commit()
db.refresh(db_blog)
return db_blog
Breaking Down the Update Logic
Find First: We're using the same pattern as our get function - find the blog by slug using SQLModel's select()
and where()
.
The None Check: If the blog doesn't exist, we return None
instead of throwing an exception right here in the CRUD layer. This keeps our separation of concerns clean - the CRUD layer handles data operations, the route layer handles HTTP responses.
Field by Field Updates: Here's where it gets interesting. We're manually updating each field:
db_blog.title = blog.title
db_blog.slug = blog.slug
db_blog.content = blog.content
The Commit Dance: After updating the fields, we call db.commit()
to save changes to the database, then db.refresh(db_blog)
to get any updated fields back e.g. the timestamp fields of created_at, updated_at.
The Route Layer
Now let's add the PUT endpoint to apis/v1/blog.py:
@router.put("/blogs/{slug}", response_model=ShowBlog)
def update_blog(slug: str, blog: CreateBlog, db: Session = Depends(get_db)):
blog = update_blog_by_slug(slug, blog, db)
if not blog:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blog not found")
return blog
PUT vs PATCH: We're using PUT here, which in REST conventions means "replace the entire resource." If we wanted partial updates, we'd use PATCH. But for simplicity, full replacement works fine for most scenarios.
Error Handling: If the CRUD function returns None
, we throw a 404. Otherwise, FastAPI automatically serializes our updated blog using the ShowBlog
schema.
Slug Updates Are Dangerous: Notice we allow updating the slug? That means the URL someone used to access the blog could change after they update it. That's... weird. In real apps, you might want to keep slugs immutable or handle redirects.
No Validation on Duplicate Slugs: What happens if someone updates their blog to use a slug that already exists? Database constraint violation! We should probably check for that.
The Manual Field Assignment Problem
That manual field-by-field update is going to get annoying fast. What if you add 10 more fields to your blog model? You'll have to remember to update this function every time.
Here's a cleaner approach using SQLModel's syntax:
def update_blog_by_slug(slug: str, blog: CreateBlog, db: Session) -> Optional[Blog]:
statement = select(Blog).where(Blog.slug == slug)
result = db.exec(statement)
db_blog = result.first()
if not db_blog:
return None
# Update only the fields that were provided
blog_data = blog.dict(exclude_unset=True)
for field, value in blog_data.items():
setattr(db_blog, field, value)
db.commit()
db.refresh(db_blog)
return db_blog
But that's getting fancy - let's stick with explicit updates for now. Clear is better than clever!
Testing Your Update
You should get back the updated blog with all your changes applied!