Migrating from SQLAlchemy¶
This guide helps you migrate from SQLAlchemy to Ferro.
Quick Comparison¶
| Feature | SQLAlchemy 2.0 | Ferro |
|---|---|---|
| Model Definition | Declarative Base | Pydantic Model |
| Queries | select() |
.where() |
| Sessions | Required | Not needed |
| Async | Native | Native |
| Migrations | Alembic | Alembic |
Model Definition¶
SQLAlchemy¶
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(unique=True)
email: Mapped[str]
Ferro¶
from ferro import Field, Model
class User(Model):
id: int | None = Field(default=None, primary_key=True)
username: str = Field(unique=True)
email: str
Queries¶
Fetch All¶
# SQLAlchemy
from sqlalchemy import select
async with session() as db:
result = await db.execute(select(User))
users = result.scalars().all()
# Ferro
users = await User.all()
Filtering¶
# SQLAlchemy
stmt = select(User).where(User.age >= 18)
result = await db.execute(stmt)
users = result.scalars().all()
# Ferro
users = await User.where(User.age >= 18).all()
Get by primary key¶
SQLAlchemy’s session.get(User, pk) returns None when the row is missing. Ferro’s await User.get(pk) returns User and raises ModelDoesNotExist when absent. Use await User.get_or_none(pk) for the optional pattern.
# SQLAlchemy
user = await session.get(User, 1)
# Ferro — raises if missing
from ferro import ModelDoesNotExist
try:
user = await User.get(1)
except ModelDoesNotExist:
user = None
# Ferro — optional (like session.get when no row)
user = await User.get_or_none(1)
Relationships¶
One-to-Many¶
# SQLAlchemy
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
posts: Mapped[List["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="posts")
# Ferro
from typing import Annotated
from ferro import BackRef, Field, ForeignKey, Model, Relation
class User(Model):
id: int | None = Field(default=None, primary_key=True)
posts: Relation[list["Post"]] = BackRef()
class Post(Model):
id: int | None = Field(default=None, primary_key=True)
author: Annotated[User, ForeignKey(related_name="posts")]
Creating Records¶
# SQLAlchemy
async with session() as db:
user = User(username="alice", email="alice@example.com")
db.add(user)
await db.commit()
# Ferro
user = await User.create(username="alice", email="alice@example.com")
Transactions¶
# SQLAlchemy
async with session.begin():
user = User(username="alice")
db.add(user)
# Auto-commits on exit
# Ferro
from ferro import transaction
async with transaction():
user = await User.create(username="alice")
# Auto-commits on exit
Migration Checklist¶
- [ ] Install Ferro:
pip install ferro-orm - [ ] Replace SQLAlchemy models with Ferro models
- [ ] Update queries to use Ferro's
.where()API - [ ] Remove session management (Ferro doesn't use sessions)
- [ ] Update relationship syntax
- [ ] Test thoroughly
- [ ] Update Alembic
env.pyto use Ferro'sget_metadata()
Key Differences¶
- No Sessions: Ferro manages connections automatically
- Pydantic Models: Ferro models are Pydantic, get validation for free
- Simpler API: Fewer concepts to learn
- Better Performance: Rust engine for bulk operations