Primary Keys and Column Types
Primary keys
Section titled “Primary keys”A primary key uniquely identifies a row.
Most tables use an auto-incrementing integer id:
id = db.Column(db.Integer, primary_key=True)Common column types
Section titled “Common column types”db.Integerdb.String(length)db.Textdb.Booleandb.DateTimedb.Floatdb.Numeric
Example:
created_at = db.Column(db.DateTime, nullable=False)Constraints
Section titled “Constraints”nullable=False(required)unique=True(unique constraint)index=True(create an index for faster lookups)
Example:
email = db.Column(db.String(120), unique=True, index=True, nullable=False)Defaults
Section titled “Defaults”You can set default values:
is_active = db.Column(db.Boolean, default=True, nullable=False)For timestamps, you typically use a callable (not a fixed time):
import datetime
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False)Foreign keys (preview)
Section titled “Foreign keys (preview)”Relationships depend on foreign keys:
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)We’ll build relationships later in this phase.
The type annotation decides nullability
Section titled “The type annotation decides nullability”In SQLAlchemy 2.0 style, Mapped[T] is not documentation — the compiler of your schema
reads it:
class Note(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] # NOT NULL
body: Mapped[str | None] # nullable
views: Mapped[int] = mapped_column(default=0)Measured schema:
| column | type | nullable | primary key |
|---|---|---|---|
id | INTEGER | no | yes |
title | VARCHAR | no | no |
body | VARCHAR | yes | no |
views | INTEGER | no | no |
flowchart LR A["Mapped[str]"] --> N["NOT NULL"] B["Mapped[str | None]"] --> Y["NULL allowed"] C["mapped_column(primary_key=True)"] --> P["PRIMARY KEY
implies NOT NULL and unique"]
Adding | None is the whole difference between a column that must have a value and one
that may not. It is easy to miss in review, and the database will enforce whichever you
wrote.
Primary keys
Section titled “Primary keys”An integer primary key is assigned by the database, which is why it does not exist until the row is written:
u = User(name="ada", age=36)
db.session.add(u)
print(u.id) # None <- nothing has been INSERTed yet
db.session.commit()
print(u.id) # 1 <- the database assigned itMeasured exactly that. Code that needs the id — to build a URL, to write a related row —
must commit (or flush()) first.
Types worth choosing deliberately
Section titled “Types worth choosing deliberately”| SQLAlchemy | use for | note |
|---|---|---|
String(n) | short text | the length is enforced by most databases, ignored by SQLite |
Text | long text | no length limit |
Integer | counts, ids | |
Numeric(10, 2) | money | exact decimal arithmetic |
Float | measurements | binary floating point, do not use for currency |
Boolean | flags | stored as an integer in SQLite |
DateTime(timezone=True) | timestamps | store UTC, convert on display |
Constraints belong in the database
Section titled “Constraints belong in the database”name: Mapped[str] = mapped_column(sa.String(20), unique=True)IntegrityError: UNIQUE constraint failed: user.nameAn application check (“does this name exist?”) races with other requests — two can both find nothing and both insert. The database constraint is the only check that cannot be raced. Keep the friendly check for a good error message, and let the constraint be the guarantee.
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
What is the difference between title: Mapped[str] and body: Mapped[str | None]?
Measured: title came out nullable=False and body nullable=True. The annotation drives the schema, so adding or omitting | None changes what the database enforces.
pch.quizShowAnswer
B — the first compiles to NOT NULL and the second allows NULL — Measured: title came out nullable=False and body nullable=True. The annotation drives the schema, so adding or omitting | None changes what the database enforces.
-
After db.session.add(u) but before commit, what is u.id for an integer primary key?
Measured None before commit and 1 after. Anything needing the id — a URL, a related row — must commit or flush first.
pch.quizShowAnswer
B — None, because no INSERT has run and the database assigns the value — Measured None before commit and 1 after. Anything needing the id — a URL, a related row — must commit or flush first.
-
Why store a currency amount in Numeric(10, 2) rather than Float?
0.1 + 0.2 is not 0.3 in binary floating point. Numeric maps to an exact decimal type and returns Decimal in Python. A rounding error in stored money cannot be corrected after the fact.
pch.quizShowAnswer
B — Float is binary floating point and cannot represent decimal fractions exactly, so amounts drift — 0.1 + 0.2 is not 0.3 in binary floating point. Numeric maps to an exact decimal type and returns Decimal in Python. A rounding error in stored money cannot be corrected after the fact.
-
Why rely on a UNIQUE constraint rather than checking for an existing row first?
Measured the constraint firing as IntegrityError: UNIQUE constraint failed: user.name. Keep the application check for a good message, and let the constraint be the guarantee.
pch.quizShowAnswer
B — two concurrent requests can both find nothing and both insert; only the database constraint cannot be raced — Measured the constraint firing as IntegrityError: UNIQUE constraint failed: user.name. Keep the application check for a good message, and let the constraint be the guarantee.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading