5.0 KiB
SQLAlchemy 2.0 Tutorial (Recipe Scraper Edition)
This guide covers SQLAlchemy 2.0, the industry-standard SQL toolkit and Object-Relational Mapper (ORM) for Python. We'll use the Recipe Web Scraper project models as our primary examples.
1. What is SQLAlchemy?
SQLAlchemy has two main components:
- Core: A SQL abstraction layer (SQL Expression Language, Schema definitions, Engine).
- ORM: A layer on top of Core that maps Python classes to database tables.
In this project, we primarily use the ORM to treat recipes and ingredients as Python objects.
2. Defining Models (The Modern Way)
SQLAlchemy 2.0 introduced a type-hint-centric way to define models using Mapped and mapped_column.
The Base Class
All models inherit from a common Base class created from DeclarativeBase.
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
Example: The Recipe Model
from sqlalchemy import String, Integer, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
class Recipe(Base):
__tablename__ = "recipes" # Name of the table in the DB
# Primary Key
id: Mapped[int] = mapped_column(primary_key=True)
# Simple Columns (SQLAlchemy infers types from Mapped[T])
url: Mapped[str] = mapped_column(String, unique=True, index=True)
title: Mapped[str]
total_time: Mapped[int | None] # Optional column (nullable=True)
# Column with a default value
scraped_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationships (Defined in section 4)
ingredients: Mapped[list["Ingredient"]] = relationship(back_populates="recipe")
3. Engine and Session
The Engine
The Engine is the starting point for any SQLAlchemy application. It manages a pool of connections to the database.
from sqlalchemy import create_engine
# SQLite: The '///' means relative path to the current directory
engine = create_engine("sqlite:///recipes.db", echo=True)
# echo=True logs all SQL commands to the terminal (great for debugging)
Creating Tables
You can tell SQLAlchemy to create all tables defined in your models:
Base.metadata.create_all(engine)
The Session
The Session handles the conversation with the database. Use sessionmaker to create a factory for sessions.
from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(bind=engine)
# Use as a context manager to ensure the connection is closed
with SessionLocal() as session:
# do work here
pass
4. Relationships (1-to-Many)
In our project, one Recipe has many Ingredients.
Foreign Key
The "child" table (Ingredient) must have a column pointing to the "parent" table (Recipe).
from sqlalchemy import ForeignKey
class Ingredient(Base):
__tablename__ = "ingredients"
id: Mapped[int] = mapped_column(primary_key=True)
# Links to 'recipes.id'
recipe_id: Mapped[int] = mapped_column(ForeignKey("recipes.id"))
text: Mapped[str]
# Back-reference to the parent Recipe object
recipe: Mapped["Recipe"] = relationship(back_populates="ingredients")
Cascades
cascade="all, delete-orphan" ensures that if you delete a Recipe, all its Ingredients are also deleted automatically.
5. CRUD Operations
Create (Insert)
with SessionLocal() as session:
new_recipe = Recipe(title="Pasta Carbonara", url="https://example.com/pasta")
session.add(new_recipe)
session.commit() # Save to DB
Read (Select)
from sqlalchemy import select
with SessionLocal() as session:
# 1. Get by ID
recipe = session.get(Recipe, 1)
# 2. Filter by column
stmt = select(Recipe).where(Recipe.title == "Pasta Carbonara")
result = session.execute(stmt).scalars().first()
# 3. Get all
all_recipes = session.query(Recipe).all() # Older syntax, still common
Update
with SessionLocal() as session:
recipe = session.get(Recipe, 1)
recipe.title = "Authentic Pasta Carbonara"
session.commit()
Delete
with SessionLocal() as session:
recipe = session.get(Recipe, 1)
session.delete(recipe)
session.commit()
6. Common Pitfalls
- Lazy Loading: By default, SQLAlchemy doesn't load relationships until you access them. This can cause "N+1" performance issues. Use
joinedloadto fetch everything in one query. - Session Lifecycle: Always use a context manager (
with session:) or close your sessions manually. - Commit vs Flush:
session.flush()sends changes to the DB but doesn't permanentize them.session.commit()makes them permanent.
7. Next Steps: Migrations with Alembic
As your models change (e.g., you add a rating column), you shouldn't just delete the DB and start over. Alembic is the tool used to handle database migrations.
Install it with: pip install alembic