Relationships in SQLAlchemy

PIN

I got curious when I was looking over a Claude designed model for my weight tracker app. My intent was to learn why it didn’t full out normalize some tables. It scoffed and told me not to over-engineer my tables due to the scope of the audience. I might have made up the scoffing part, but my feelings were still hurt. Anyway, I noticed the relationship() and wanted to know more. Is it like a foreign key? That’s when I learned how SQLAlchemy was using a relationship feature and abstracting some work behind the scenes.

class WeightEntry(Base):
    __tablename__ = "weight_entries"

    id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    user_id: Mapped[str] = mapped_column(
        String, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
    )
    weight_value: Mapped[float] = mapped_column(Float, nullable=False)
    recorded_at: Mapped[date] = mapped_column(Date, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    user = relationship("User", back_populates="weight_entries")

Here’s the tables for reference.

It does what it says. It creates a relationship and lets you move through objects without have to write JOIN queries. For example:

without the relationship()

# without the relationship()
user = db.query(User).filter(User.id == entry.user_id).first()

# with the relationship
entry.user

The SQL still happens, but it’s done behind the scene.

There’s another interesting thing we get with the relationship….cascading. because there’s now a relationship between the user of users and entries of weight_entries, we can use this relationship to run through the entries automatically. For example in the user model there is:

weight_entries = relationship("WeightEntry", back_populates="user", cascade="all, delete-orphan")
class User(Base):
    __tablename__ = "users"

    id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    hashed_password: Mapped[str] = mapped_column(Text, nullable=False)
    weight_unit: Mapped[str] = mapped_column(String(3), nullable=False, default="lbs")
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    weight_entries = relationship("WeightEntry", back_populates="user", cascade="all, delete-orphan")

So now when someone wants to delete their weight profile

db.delete(current_user)

runs through and deletes all their entries. Cool.

I had originally intended this to be about why Claude chose not to normalize. I’m really curious about that, but I got side tracked on this relationship thing. 🙂