Skip to content

Support SQLAlchemy 2's DeclarativeBase and MappedAsDataclass #1140

@tadams42

Description

@tadams42

I've been playing with mapped_column and MappedAsDataclass (new stuff in SQLAlchemy 2.x):

Declarative Table with mapped_column()
Declarative Dataclass Mapping

Example:

mkdir sqlaplayground
cd sqlaplayground
pyenv local 3.11 
python -m venv .venv 
source .venv/bin/activate
pip install -U pip wheel setuptools
cat requirements.txt
# sqlalchemy >= 2.0.0b3
# flask-sqlalchemy @ git+https://github.com/pallets-eco/flask-sqlalchemy@3.0.x
# flask >= 2.0.0
pip install -r requirements.txt
from __future__ import annotations

from typing import Optional

from sqlalchemy import ForeignKey, String, create_engine, orm
from sqlalchemy.orm import (
    DeclarativeBase, Mapped, MappedAsDataclass, mapped_column, sessionmaker
)

CONFIG = {
    "SQLALCHEMY_DATABASE_URI": "sqlite+pysqlite://",
}

Engine = create_engine(CONFIG["SQLALCHEMY_DATABASE_URI"], future=True)
Session = sessionmaker(bind=Engine, future=True)
session = Session()


class BaseModel(MappedAsDataclass, DeclarativeBase):
    """subclasses will be converted to dataclasses"""

    pass


class Book(BaseModel):
    __tablename__ = "books"

    id: Mapped[Optional[int]] = mapped_column(
        init=False, primary_key=True, autoincrement=True
    )

    title: Mapped[Optional[str]] = mapped_column(String(length=64), default=None)

    author_id: Mapped[Optional[int]] = mapped_column(
        ForeignKey("authors.id"), nullable=False, index=True, default=None
    )
    author: Mapped[Optional[Author]] = orm.relationship(
        "Author", uselist=False, back_populates="books", default=None
    )


class Author(BaseModel):
    __tablename__ = "authors"

    id: Mapped[Optional[int]] = mapped_column(
        init=False, primary_key=True, autoincrement=True
    )
    name: Mapped[Optional[str]] = mapped_column(String(length=64), default=None)
    books: Mapped[list[Book]] = orm.relationship(
        "Book", uselist=True, back_populates="author", default_factory=list
    )

BaseModel.metadata.create_all(Engine)

book = Book(title="42", author=Author(name="Name"))
session.add(book)
session.commit()

This rather verbose declaration of models gives us some nice things:

  • more precise mypy and PyRight static typechecking.
  • dataclass-like __init__
repr(book)
# "Book(id=1, title='42', author_id=1, author=Author(id=1, name='Name', books=[...]))"
?book.__init__
# book.__init__(title=None, author_id=None, author=None)

First try at using my BaseModel with Flask-SQLAlchemy gets me into trouble with metaclass inheritance:

import flask
import flask_sqlalchemy


app = flask.Flask(__name__)
app.config.from_mapping(CONFIG)

db = flask_sqlalchemy.SQLAlchemy(model_class=BaseModel)

# TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) 
# subclass of the metaclasses of all its bases

which is fair enough...

I did try few things from Advanced Customization but so far came empty handed.

Can Flask-SQLAlchemy support this "new" declarative models?
Should it?
Maybe avoid this problem by somehow replacing metaclass magic with __init_subclass__ from
PEP 487 - Simpler customisation of class creation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions