Skip to content

Relations

Foreign keys define relationships between tables. In Embar, you create foreign keys using the .fk() method on any column type.

Basic Foreign Key

Use the .fk() method to reference a column in another table:

from embar.column.common import Integer, Text
from embar.table import Table

class User(Table):
    id: Integer = Integer(primary=True)
    email: Text = Text()

class Message(Table):
    id: Integer = Integer(primary=True)
    user_id: Integer = Integer().fk(lambda: User.id)
    content: Text = Text()

The lambda syntax (lambda: User.id) is required because User might not be defined yet when Python evaluates the Message class body.

This generates:

"user_id" INTEGER REFERENCES "user"("id")

On Delete Behavior

You can specify what happens when the referenced row is deleted using the on_delete parameter:

class Message(Table):
    id: Integer = Integer(primary=True)
    user_id: Integer = Integer().fk(
        lambda: User.id,
        on_delete="cascade"
    )
    content: Text = Text()

Available Options

The on_delete parameter accepts these string literals:

  • "cascade": Delete this row when the referenced row is deleted
  • "set null": Set this column to NULL when the referenced row is deleted
  • "restrict": Prevent deletion of the referenced row if this row exists
  • "no action": Same as restrict (default SQL behavior)
  • "set default": Set this column to its default value when the referenced row is deleted

Cascade Example

When a user is deleted, all their messages are deleted:

class Message(Table):
    user_id: Integer = Integer().fk(
        lambda: User.id,
        on_delete="cascade"
    )

Generates:

"user_id" INTEGER REFERENCES "user"("id") ON DELETE cascade

Set Null Example

When a user is deleted, the foreign key is set to NULL:

class Message(Table):
    user_id: Integer = Integer().fk(
        lambda: User.id,
        on_delete="set null"
    )

Make sure the column allows NULL values (don't use not_null=True).

Generates:

"user_id" INTEGER REFERENCES "user"("id") ON DELETE set null

Multi-Level Relations

You can chain foreign keys across multiple tables:

class Organization(Table):
    id: Integer = Integer(primary=True)
    name: Text = Text()

class User(Table):
    id: Integer = Integer(primary=True)
    org_id: Integer = Integer().fk(lambda: Organization.id)
    email: Text = Text()

class Message(Table):
    id: Integer = Integer(primary=True)
    user_id: Integer = Integer().fk(lambda: User.id, on_delete="cascade")
    content: Text = Text()

Composite Constraints

For more complex relationships, combine foreign keys with other column options:

class Message(Table):
    id: Integer = Integer(primary=True)
    user_id: Integer = Integer(not_null=True).fk(
        lambda: User.id,
        on_delete="cascade"
    )
    content: Text = Text(not_null=True)

This ensures every message must have a user, and messages are deleted when their user is deleted.

Working with Foreign Keys

When querying, use joins to fetch related data:

from typing import Annotated
from pydantic import BaseModel
from embar.db.pg import AsyncPgDb
from embar.query.where import Eq

class UserWithMessages(BaseModel):
    email: Annotated[str, User.email]
    messages: Annotated[list[str], Message.content.many()]

async def main():
    db = AsyncPgDb(...)
    users = await (
        db.select(UserWithMessages)
        .from_(User)
        .left_join(Message, Eq(User.id, Message.user_id))
        .group_by(User.id)
    )

For more on querying related data, see Joins.

Next Steps

  • Learn about Constraints like indexes and unique constraints
  • Explore Joins for querying related data