SQLModel

repository·main·Indexed 12 days ago

https://github.com/fastapi/sqlmodel

A library for interacting with SQL databases from Python using Python objects. SQLModel combines Pydantic and SQLAlchemy to minimize code duplication, providing a thin layer that is highly compatible with FastAPI. It allows developers to define data models for validation and serialization while simultaneously serving as SQLAlchemy models for database schema management.

Tokens
50.1K
Snippets
152
Records
191
Agent score
91%

What's inside SQLModel

  1. What is an ORM (Object-Relational Mapper)?

    main

    An ORM (Object-Relational Mapper) is a library that translates between SQL (Relational) and Python code (Objects).

    • Object: Refers to Object-Oriented Programming (classes and instances).
    • Relational: Refers to SQL databases where data is stored in tables (relations).
    • Mapper: Refers to the function that maps data from one set (SQL rows) to another (Python objects).

    SQLModel acts as this mapper, allowing you to define your database schema using Python classes.

  2. How Relationship Attributes work

    main

    Relationship attributes in SQLModel provide an abstraction over foreign keys. While a Field (like team_id) stores the actual integer ID in the database column, a Relationship attribute (like team) allows you to access the entire related object directly.

    Key Characteristics:

    • Not a Column: They do not represent a column in the database table.
    • Object Access: Accessing a relationship attribute returns the related instance (or a list of instances) rather than a scalar value.
    • Nullability: If the underlying foreign key is nullable (e.g., team_id: int | None), the relationship attribute must also be annotated as nullable (e.g., team: Team | None) to avoid errors when no relationship exists.
    • Collections: For one-to-many relationships, the attribute is typically annotated as a list (e.g., list[Hero]).
    # Example of accessing a relationship attribute
    if hero.team:
        print(hero.team.name)
  3. Identify different database interaction models

    main

    Databases can be categorized by how your application interacts with them:

    1. Single file database: The database is managed as a single file (e.g., heroes.db). Your code interacts with this file through a management layer. SQLite is a primary example.
    2. Server database: The database runs as a separate application on a server. Your code communicates with this server application rather than reading/modifying data files directly. The server handles the optimized file management. Examples include PostgreSQL, MySQL, and MongoDB.
    3. Distributed servers: A group of server applications running on different machines working together to handle large amounts of data. Your code communicates with one or more of these distributed servers.
  4. Define SQLModel relationships between tables

    main

    To create relationships between models (e.g., a Team having many Heroes), use the Relationship() function.

    Key rules for relationships:

    1. Table Models Only: Relationship() attributes should only be defined in table models (models that inherit from SQLModel with table=True). This allows SQLModel to use SQLAlchemy to handle automatic data fetching.
    2. Foreign Keys: Use the foreign_key parameter in a field definition to link a model to another table. You can define foreign_key in a base data model (where table=False), but it only takes effect when that field is inherited by a model where table=True.
    3. Relationship Attributes: These allow you to access related objects directly (e.g., hero.team or team.heroes) instead of manually querying by ID.
    from typing import List, Optional
    from sqlmodel import Field, Relationship, SQLModel
    
    class TeamBase(SQLModel):
        name: str
        headquarters: str
    
    class Team(TeamBase, table=True):
        id: Optional[int] = Field(default=None, primary_key=True)
        # Relationship to the Hero table
        heroes: List["Hero"] = Relationship(back_populates="team")
    
    class HeroBase(SQLModel):
        name: str
        secret_name: str
        # team_id can be defined in the base model as an optional integer
        team_id: Optional[int] = Field(default=None, foreign_key="team.id")
    
    class Hero(HeroBase, table=True):
        id: Optional[int] = Field(default=None, primary_key=True)
        # Relationship to the Team table
        team: Optional[Team] = Relationship(back_populates="heroes")
  5. How to use pytest fixtures for FastAPI and SQLModel testing

    main

    When testing FastAPI applications with SQLModel, it is best practice to use pytest fixtures to manage the lifecycle of the database session and the TestClient.

    By separating the setup into two distinct fixtures—one for the session and one for the client—you gain flexibility:

    1. The session fixture: Manages the database connection and transaction lifecycle for the testing database.
    2. The client fixture: Provides a TestClient instance configured to use the testing database (often by overriding dependencies).

    Key Pattern: Requiring Multiple Fixtures If a test needs to both interact with the API via the client AND manipulate the database directly (e.g., to seed data before a request), you should require both the client and the session fixtures in your test function. Because fixtures can depend on other fixtures, the client fixture and your test function will share the same underlying session instance, ensuring data consistency during the test.

    # Example pattern for a test requiring both client and session
    
    def test_read_heroes(client: TestClient, session: Session):
        # 1. Seed the database using the session fixture
        hero_1 = Hero(name="Deadpool", secret_identity="Wade Wilson")
        session.add(hero_1)
        session.commit()
        session.refresh(hero_1)
    
        # 2. Use the client fixture to make the API request
        response = client.get("/heroes/")
    
        # 3. Assert the results
        assert response.status_code == 200
        assert response.json() == [{"id": hero_1.id, "name": "Deadpool", "secret_identity": "Wade Wilson"}]
  6. The SQLModel Advantage: Dual-purpose Models

    main

    A key benefit of using SQLModel with FastAPI is that your model classes serve two purposes simultaneously:

    1. Pydantic Model: FastAPI uses the model to perform automatic data validation and conversion from JSON request bodies into Python objects.
    2. SQLAlchemy Model: The same object can be used directly within a Session to create or manipulate rows in the database.

    This eliminates the need to define separate schemas for your API (Pydantic) and your database (SQLAlchemy), reducing code duplication.

  7. Distinguish between Data Models and Table Models

    main

    In SQLModel, the distinction between a data model and a table model is controlled by the table configuration parameter.

    • Data Models (table=False, which is the default): These act as Pydantic models. They are used for API request/response schemas, data validation, and documentation. They do not create tables in the database.
    • Table Models (table=True): These act as both Pydantic models and SQLAlchemy models. They are used to define the actual structure of your database tables.

    Using multiple models allows you to control exactly which fields are visible to the API client (e.g., hiding a hashed_password field in a HeroPublic model while keeping it in the Hero table model).

  8. How the Engine and Session work together

    main

    In SQLModel, there is a distinction between the Engine and the Session:

    1. Engine: A single, shared object for the entire application. It is responsible for communicating with the database and handling connections (e.g., to PostgreSQL or MySQL).
    2. Session: A lightweight object created for a specific group of operations (e.g., one session per web request). The session sits on top of the engine and manages a local 'in-memory' state of objects.

    When you add objects to a session, they are not immediately sent to the database. Instead, the session tracks them, and when you commit the session, it uses the engine to send all changes to the database in a single, efficient transaction.

  9. Create a Table Model Class with SQLModel

    main

    To represent a database table, create a class that inherits from SQLModel. Use the table=True configuration to designate it as a table model (which maps to a database table). If you omit table=True, the class acts as a data model (useful for validation but not for database storage).

    Each instance of this class represents a single row in the database table.

    from sqlmodel import SQLModel, Field
    
    class Hero(SQLModel, table=True):
        id: int | None = Field(default=None, primary_key=True)
        name: str
        secret_name: str
        age: int | None = None
  10. Create an independent Update model for partial updates

    main

    When designing models for updates, it is often better to create an independent model (e.g., HeroUpdate) rather than using complex inheritance.

    For a partial update (like a PATCH operation), all fields in the update model should be optional (having a default value of None). This allows the client to send only the specific fields they wish to change. Because the original base model might have required fields, you cannot simply inherit from it; you must define the update model with optional fields explicitly.

    from typing import Optional
    from sqlmodel import SQLModel
    
    class HeroUpdate(SQLModel):
        name: Optional[str] = None
        secret_name: Optional[str] = None
        age: Optional[int] = None
  11. Define required vs optional fields in API contracts

    main

    To create a clear API contract for clients (frontend, mobile, etc.), use specific type annotations in your SQLModel classes:

    • For Creation (HeroCreate): Omit fields that the database generates automatically (like id). This prevents clients from attempting to overwrite existing IDs.
    • For Responses (HeroPublic): Declare fields that are guaranteed to exist in the database as required (e.g., id: int). This allows client-side code generators to avoid unnecessary null-checks.
    • For Database (Hero): Use Optional[int] = Field(default=None, primary_key=True) for the id so the model can exist in memory before it is persisted and assigned an ID by the database.

    By using response_model in FastAPI, the outgoing data is validated against your public schema, which also acts as a security filter to remove any fields not explicitly declared in the response model (like hashed passwords).

  12. Understand the difference between `select()` and `.join()`

    main

    In SQLModel, select() and .join() serve distinct purposes that mirror SQL behavior:

    1. select(...): Defines which columns/data you want to retrieve. If you want to access attributes of a joined model (e.g., hero.team.name), you must include that model in the select() call.
    2. .join(...): Defines how tables are connected and how rows are filtered. It establishes the relationship between tables so the database knows how to match them.

    Filtering without selecting data

    If you call .join(Team) but only select(Hero), you can use the Team model in a .where() clause to filter the results, but the resulting objects will not contain the Team data.

    Filtering and selecting data

    To both filter by a related table AND retrieve its data, you must include both models in the select() call and use .join() to link them.

    # Scenario 1: Filter by Team, but ONLY get Hero data
    # The 'team' attribute on the Hero object will be empty/None
    statement = select(Hero).join(Team).where(Team.name == 'Preventers')
    
    # Scenario 2: Filter by Team AND get both Hero and Team data
    # The 'team' attribute on the Hero object will be populated
    statement = select(Hero, Team).join(Team).where(Team.name == 'Preventers')