Skip to content

Commit 9f3bf94

Browse files
authored
Support the SQLAlchemy 2.0 DeclarativeBase models (#1215)
2 parents 564a96f + fdeec1d commit 9f3bf94

23 files changed

+1118
-276
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Version 3.1.0
33

44
Unreleased
55

6+
- Add support for the SQLAlchemy 2.x API via ``model_class`` parameter. :issue:`1140`
7+
- Bump minimum version of SQLAlchemy to 2.0.16.
68
- Remove previously deprecated code.
79
- Pass extra keyword arguments from ``get_or_404`` to ``session.get``. :issue:`1149`
810

README.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ A Simple Example
2929
3030
from flask import Flask
3131
from flask_sqlalchemy import SQLAlchemy
32+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
3233
3334
app = Flask(__name__)
3435
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///example.sqlite"
35-
db = SQLAlchemy(app)
36+
37+
class Base(DeclarativeBase):
38+
pass
39+
40+
db = SQLAlchemy(app, model_class=Base)
3641
3742
class User(db.Model):
38-
id = db.Column(db.Integer, primary_key=True)
39-
username = db.Column(db.String, unique=True, nullable=False)
43+
id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
44+
username: Mapped[str] = mapped_column(db.String, unique=True, nullable=False)
4045
4146
with app.app_context():
4247
db.create_all()

docs/api.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ Model
3232
If the ``__table__`` or ``__tablename__`` is set explicitly, that will be used
3333
instead.
3434

35+
Metaclass mixins (SQLAlchemy 1.x)
36+
---------------------------------
37+
38+
If your code uses the SQLAlchemy 1.x API (the default for code that doesn't specify a ``model_class``),
39+
then these mixins are automatically applied to the ``Model`` class.
40+
3541
.. autoclass:: DefaultMeta
3642

3743
.. autoclass:: BindMetaMixin

docs/config.rst

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -151,31 +151,6 @@ only need to use :data:`SQLALCHEMY_DATABASE_URI` and :data:`SQLALCHEMY_ENGINE_OP
151151
in that engine's options.
152152

153153

154-
Using custom MetaData and naming conventions
155-
--------------------------------------------
156-
157-
You can optionally construct the :class:`.SQLAlchemy` object with a custom
158-
:class:`~sqlalchemy.schema.MetaData` object. This allows you to specify a custom
159-
constraint `naming convention`_. This makes constraint names consistent and predictable,
160-
useful when using migrations, as described by `Alembic`_.
161-
162-
.. code-block:: python
163-
164-
from sqlalchemy import MetaData
165-
from flask_sqlalchemy import SQLAlchemy
166-
167-
db = SQLAlchemy(metadata=MetaData(naming_convention={
168-
"ix": 'ix_%(column_0_label)s',
169-
"uq": "uq_%(table_name)s_%(column_0_name)s",
170-
"ck": "ck_%(table_name)s_%(constraint_name)s",
171-
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
172-
"pk": "pk_%(table_name)s"
173-
}))
174-
175-
.. _naming convention: https://docs.sqlalchemy.org/core/constraints.html#constraint-naming-conventions
176-
.. _Alembic: https://alembic.sqlalchemy.org/en/latest/naming.html
177-
178-
179154
Timeouts
180155
--------
181156

docs/customizing.rst

Lines changed: 39 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,26 @@ joined-table inheritance.
2121

2222
.. code-block:: python
2323
24-
from flask_sqlalchemy.model import Model
25-
import sqlalchemy as sa
26-
import sqlalchemy.orm
24+
from sqlalchemy import Integer, String, ForeignKey
25+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, declared_attr
2726
28-
class IdModel(Model):
29-
@sa.orm.declared_attr
27+
class Base(DeclarativeBase):
28+
@declared_attr.cascading
29+
@classmethod
3030
def id(cls):
3131
for base in cls.__mro__[1:-1]:
3232
if getattr(base, "__table__", None) is not None:
33-
type = sa.ForeignKey(base.id)
34-
break
35-
else:
36-
type = sa.Integer
33+
return mapped_column(ForeignKey(base.id), primary_key=True)
34+
else:
35+
return mapped_column(Integer, primary_key=True)
3736
38-
return sa.Column(type, primary_key=True)
39-
40-
db = SQLAlchemy(model_class=IdModel)
37+
db = SQLAlchemy(app, model_class=Base)
4138
4239
class User(db.Model):
43-
name = db.Column(db.String)
40+
name: Mapped[str] = mapped_column(String)
4441
4542
class Employee(User):
46-
title = db.Column(db.String)
43+
title: Mapped[str] = mapped_column(String)
4744
4845
4946
Abstract Models and Mixins
@@ -56,28 +53,49 @@ they are created or updated.
5653
.. code-block:: python
5754
5855
from datetime import datetime
56+
from sqlalchemy import DateTime, Integer, String
57+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, declared_attr
5958
6059
class TimestampModel(db.Model):
6160
__abstract__ = True
62-
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
63-
updated = db.Column(db.DateTime, onupdate=datetime.utcnow)
61+
created: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
62+
updated: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
6463
6564
class Author(db.Model):
66-
...
65+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
66+
username: Mapped[str] = mapped_column(String, unique=True, nullable=False)
6767
6868
class Post(TimestampModel):
69-
...
69+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
70+
title: Mapped[str] = mapped_column(String, nullable=False)
7071
7172
This can also be done with a mixin class, inheriting from ``db.Model`` separately.
7273

7374
.. code-block:: python
7475
7576
class TimestampMixin:
76-
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
77-
updated = db.Column(db.DateTime, onupdate=datetime.utcnow)
77+
created: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
78+
updated: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
7879
7980
class Post(TimestampMixin, db.Model):
80-
...
81+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
82+
title: Mapped[str] = mapped_column(String, nullable=False)
83+
84+
85+
Disabling Table Name Generation
86+
-------------------------------
87+
88+
Some projects prefer to set each model's ``__tablename__`` manually rather than relying
89+
on Flask-SQLAlchemy's detection and generation. The simple way to achieve that is to
90+
set each ``__tablename__`` and not modify the base class. However, the table name
91+
generation can be disabled by setting `disable_autonaming=True` in the `SQLAlchemy` constructor.
92+
93+
.. code-block:: python
94+
95+
class Base(sa_orm.DeclarativeBase):
96+
pass
97+
98+
db = SQLAlchemy(app, model_class=Base, disable_autonaming=True)
8199
82100
83101
Session Class
@@ -158,73 +176,3 @@ To customize only ``session.query``, pass the ``query_cls`` key to the
158176
.. code-block:: python
159177
160178
db = SQLAlchemy(session_options={"query_cls": GetOrQuery})
161-
162-
163-
Model Metaclass
164-
---------------
165-
166-
.. warning::
167-
Metaclasses are an advanced topic, and you probably don't need to customize them to
168-
achieve what you want. It is mainly documented here to show how to disable table
169-
name generation.
170-
171-
The model metaclass is responsible for setting up the SQLAlchemy internals when defining
172-
model subclasses. Flask-SQLAlchemy adds some extra behaviors through mixins; its default
173-
metaclass, :class:`~.DefaultMeta`, inherits them all.
174-
175-
- :class:`.BindMetaMixin`: ``__bind_key__`` sets the bind to use for the model.
176-
- :class:`.NameMetaMixin`: If the model does not specify a ``__tablename__`` but does
177-
specify a primary key, a name is automatically generated.
178-
179-
You can add your own behaviors by defining your own metaclass and creating the
180-
declarative base yourself. Be sure to still inherit from the mixins you want (or just
181-
inherit from the default metaclass).
182-
183-
Passing a declarative base class instead of a simple model base class to ``model_class``
184-
will cause Flask-SQLAlchemy to use this base instead of constructing one with the
185-
default metaclass.
186-
187-
.. code-block:: python
188-
189-
from sqlalchemy.orm import declarative_base
190-
from flask_sqlalchemy import SQLAlchemy
191-
from flask_sqlalchemy.model import DefaultMeta, Model
192-
193-
class CustomMeta(DefaultMeta):
194-
def __init__(cls, name, bases, d):
195-
# custom class setup could go here
196-
197-
# be sure to call super
198-
super(CustomMeta, cls).__init__(name, bases, d)
199-
200-
# custom class-only methods could go here
201-
202-
CustomModel = declarative_base(cls=Model, metaclass=CustomMeta, name="Model")
203-
db = SQLAlchemy(model_class=CustomModel)
204-
205-
You can also pass whatever other arguments you want to
206-
:func:`~sqlalchemy.orm.declarative_base` to customize the base class.
207-
208-
209-
Disabling Table Name Generation
210-
```````````````````````````````
211-
212-
Some projects prefer to set each model's ``__tablename__`` manually rather than relying
213-
on Flask-SQLAlchemy's detection and generation. The simple way to achieve that is to
214-
set each ``__tablename__`` and not modify the base class. However, the table name
215-
generation can be disabled by defining a custom metaclass with only the
216-
``BindMetaMixin`` and not the ``NameMetaMixin``.
217-
218-
.. code-block:: python
219-
220-
from sqlalchemy.orm import DeclarativeMeta, declarative_base
221-
from flask_sqlalchemy.model import BindMetaMixin, Model
222-
223-
class NoNameMeta(BindMetaMixin, DeclarativeMeta):
224-
pass
225-
226-
CustomModel = declarative_base(cls=Model, metaclass=NoNameMeta, name="Model")
227-
db = SQLAlchemy(model_class=CustomModel)
228-
229-
This creates a base that still supports the ``__bind_key__`` feature but does not
230-
generate table names.

docs/legacy-quickstart.rst

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
:orphan:
3+
4+
Legacy Quickstart
5+
======================
6+
7+
.. warning::
8+
This guide shows you how to initialize the extension and define models
9+
when using the SQLAlchemy 1.x style of ORM model classes. We encourage you to
10+
upgrade to `SQLAlchemy 2.x`_ to take advantage of the new typed model classes.
11+
12+
.. _SQLAlchemy 2.x: https://docs.sqlalchemy.org/en/20/orm/quickstart.html
13+
14+
Initialize the Extension
15+
------------------------
16+
17+
First create the ``db`` object using the ``SQLAlchemy`` constructor.
18+
19+
When using the SQLAlchemy 1.x API, you do not need to pass any arguments to the ``SQLAlchemy`` constructor.
20+
A declarative base class will be created behind the scenes for you.
21+
22+
.. code-block:: python
23+
24+
from flask import Flask
25+
from flask_sqlalchemy import SQLAlchemy
26+
from sqlalchemy.orm import DeclarativeBase
27+
28+
db = SQLAlchemy()
29+
30+
31+
Using custom MetaData and naming conventions
32+
--------------------------------------------
33+
34+
You can optionally construct the :class:`.SQLAlchemy` object with a custom
35+
:class:`~sqlalchemy.schema.MetaData` object. This allows you to specify a custom
36+
constraint `naming convention`_. This makes constraint names consistent and predictable,
37+
useful when using migrations, as described by `Alembic`_.
38+
39+
.. code-block:: python
40+
41+
from sqlalchemy import MetaData
42+
from flask_sqlalchemy import SQLAlchemy
43+
44+
db = SQLAlchemy(metadata=MetaData(naming_convention={
45+
"ix": 'ix_%(column_0_label)s',
46+
"uq": "uq_%(table_name)s_%(column_0_name)s",
47+
"ck": "ck_%(table_name)s_%(constraint_name)s",
48+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
49+
"pk": "pk_%(table_name)s"
50+
}))
51+
52+
.. _naming convention: https://docs.sqlalchemy.org/core/constraints.html#constraint-naming-conventions
53+
.. _Alembic: https://alembic.sqlalchemy.org/en/latest/naming.html
54+
55+
56+
57+
Define Models
58+
-------------
59+
60+
Subclass ``db.Model`` to define a model class. This is a SQLAlchemy declarative base
61+
class, it will take ``Column`` attributes and create a table.
62+
63+
.. code-block:: python
64+
65+
class User(db.Model):
66+
id = db.Column(db.Integer, primary_key=True)
67+
username = db.Column(db.String, unique=True, nullable=False)
68+
email = db.Column(db.String)
69+
70+
For convenience, the extension object provides access to names in the ``sqlalchemy`` and
71+
``sqlalchemy.orm`` modules. So you can use ``db.Column`` instead of importing and using
72+
``sqlalchemy.Column``, although the two are equivalent.
73+
74+
Unlike plain SQLAlchemy, Flask-SQLAlchemy's model will automatically generate a table name
75+
if ``__tablename__`` is not set and a primary key column is defined.
76+
The table name ``"user"`` will automatically be assigned to the model's table.
77+
78+
79+
Create the Tables
80+
-----------------
81+
82+
Defining a model does not create it in the database. Use :meth:`~.SQLAlchemy.create_all`
83+
to create the models and tables after defining them. If you define models in submodules,
84+
you must import them so that SQLAlchemy knows about them before calling ``create_all``.
85+
86+
.. code-block:: python
87+
88+
with app.app_context():
89+
db.create_all()
90+
91+
Querying the Data
92+
-----------------
93+
94+
You can query the data the same way regardless of SQLAlchemy version.
95+
See :doc:`queries` for more information about queries.

0 commit comments

Comments
 (0)