Skip to content

Commit fa8f9bf

Browse files
authored
Merge pull request pallets-eco#546 from davidism/metaclass
allow passing declarative_base as model_class
2 parents a04b971 + 9aa42b2 commit fa8f9bf

File tree

6 files changed

+346
-181
lines changed

6 files changed

+346
-181
lines changed

CHANGES

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
Changelog
22
=========
33

4+
Version 2.3.0
5+
-------------
6+
7+
In development
8+
9+
- Multiple bugs with ``__tablename__`` generation are fixed. Names will be
10+
generated for models that define a primary key, but not for single-table
11+
inheritance subclasses. Names will not override a ``declared_attr``.
12+
``PrimaryKeyConstraint`` is detected. (`#541`_)
13+
- Passing an existing ``declarative_base()`` as ``model_class`` to
14+
``SQLAlchemy.__init__`` will use this as the base class instead of creating
15+
one. This allows customizing the metaclass used to construct the base.
16+
(`#546`_)
17+
- The undocumented ``DeclarativeMeta`` internals that the extension uses for
18+
binds and table name generation have been refactored to work as mixins.
19+
Documentation is added about how to create a custom metaclass that does not
20+
do table name generation. (`#546`_)
21+
- Model and metaclass code has been moved to a new ``models`` module.
22+
``_BoundDeclarativeMeta`` is renamed to ``DefaultMeta``; the old name will be
23+
removed in 3.0. (`#546`_)
24+
- Models have a default ``repr`` that shows the model name and primary key.
25+
(`#530`_)
26+
27+
.. _#530: https://github.com/mitsuhiko/flask-sqlalchemy/pull/530
28+
.. _#541: https://github.com/mitsuhiko/flask-sqlalchemy/pull/541
29+
.. _#546: https://github.com/mitsuhiko/flask-sqlalchemy/pull/546
30+
431
Version 2.2
532
-----------
633

docs/api.rst

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ API
33

44
.. module:: flask_sqlalchemy
55

6-
This part of the documentation documents all the public classes and
7-
functions in Flask-SQLAlchemy.
8-
96
Configuration
107
`````````````
118

@@ -16,33 +13,33 @@ Models
1613
``````
1714

1815
.. autoclass:: Model
19-
:members:
16+
:members:
2017

21-
.. attribute:: __bind_key__
18+
.. attribute:: __bind_key__
2219

23-
Optionally declares the bind to use. `None` refers to the default
24-
bind. For more information see :ref:`binds`.
20+
Optionally declares the bind to use. ``None`` refers to the default
21+
bind. For more information see :ref:`binds`.
2522

26-
.. attribute:: __tablename__
23+
.. attribute:: __tablename__
2724

28-
The name of the table in the database. This is required by SQLAlchemy;
29-
however, Flask-SQLAlchemy will set it automatically if a model has a
30-
primary key defined. If the ``__table__`` or ``__tablename__`` is set
31-
explicitly, that will be used instead.
25+
The name of the table in the database. This is required by SQLAlchemy;
26+
however, Flask-SQLAlchemy will set it automatically if a model has a
27+
primary key defined. If the ``__table__`` or ``__tablename__`` is set
28+
explicitly, that will be used instead.
3229

3330
.. autoclass:: BaseQuery
34-
:members:
31+
:members:
3532

3633
Sessions
3734
````````
3835

3936
.. autoclass:: SignallingSession
40-
:members:
37+
:members:
4138

4239
Utilities
4340
`````````
4441

4542
.. autoclass:: Pagination
46-
:members:
43+
:members:
4744

4845
.. autofunction:: get_debug_queries

docs/customizing.rst

Lines changed: 117 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,75 +6,81 @@ Customizing
66
===========
77

88
Flask-SQLAlchemy defines sensible defaults. However, sometimes customization is
9-
needed. Two major pieces to customize are the Model base class and the default
10-
Query class.
9+
needed. There are various ways to customize how the models are defined and
10+
interacted with.
1111

12-
Both of these customizations are applied at the creation of the :class:`SQLAlchemy`
12+
These customizations are applied at the creation of the :class:`SQLAlchemy`
1313
object and extend to all models derived from its ``Model`` class.
1414

15+
1516
Model Class
1617
-----------
1718

18-
Flask-SQLAlchemy allows defining a custom declarative base, just like SQLAlchemy,
19-
that all model classes should extend from. For example, if all models should have
20-
a custom ``__repr__`` method::
19+
SQLAlchemy models all inherit from a declarative base class. This is exposed
20+
as ``db.Model`` in Flask-SQLAlchemy, which all models extend. This can be
21+
customized by subclassing the default and passing the custom class to
22+
``model_class``.
2123

22-
from flask_sqlalchemy import Model # this is the default declarative base
23-
from flask_sqlalchemy import SQLAlchemy
24+
The following example gives every model an integer primary key, or a foreign
25+
key for joined-table inheritance.
2426

25-
class ReprBase(Model):
26-
def __repr__(self):
27-
return "<{0} id: {1}>".format(self.__class__.__name__, self.id)
27+
.. note::
2828

29-
db = SQLAlchemy(model_class=ReprBase)
29+
Integer primary keys for everything is not necessarily the best database
30+
design (that's up to your project's requirements), this is only an example.
3031

31-
class MyModel(db.Model):
32-
...
32+
::
3333

34-
.. note::
34+
from flask_sqlalchemy import Model, SQLAlchemy
35+
import sqlalchemy as sa
36+
from sqlalchemy.ext.declarative import declared_attr, has_inherited_table
3537

36-
While not strictly necessary to inherit from :class:`flask_sqlalchemy.Model`
37-
it is encouraged as future changes may cause incompatibility.
38+
class IdModel(Model):
39+
@declared_attr
40+
def id(cls):
41+
for base in cls.__mro__[1:-1]:
42+
if getattr(base, '__table__', None) is not None:
43+
type = sa.ForeignKey(base.id)
44+
break
45+
else:
46+
type = sa.Integer
3847

39-
.. note::
48+
return sa.Column(type, primary_key=True)
4049

41-
If behavior is needed in only some models, not all, a better strategy
42-
is to use a Mixin, as exampled below.
50+
db = SQLAlchemy(model_class=IdModel)
4351

44-
While this particular example is more useful for debugging, it is possible to
45-
provide many augmentations to models that would otherwise be achieved with
46-
mixins instead. The above example is equivalent to the following::
52+
class User(db.Model):
53+
name = db.Column(db.String)
4754

48-
class ReprBase(object):
49-
def __repr__(self):
50-
return "<{0} id: {1}>".format(self.__class__.__name__, self.id)
55+
class Employee(User):
56+
title = db.Column(db.String)
5157

52-
db = SQLAlchemy()
5358

54-
class MyModel(db.Model, ReprBase):
55-
...
59+
Model Mixins
60+
------------
5661

57-
It also possible to provide default columns and properties to all models as well::
62+
If behavior is only needed on some models rather than all models, use mixin
63+
classes to customize only those models. For example, if some models should
64+
track when they are created or updated::
5865

59-
from flask_sqlalchemy import Model, SQLAlchemy
60-
from sqlalchemy import Column, DateTime
6166
from datetime import datetime
6267

63-
class TimestampedModel(Model):
64-
created_at = Column(DateTime, default=datetime.utcnow)
68+
class TimestampMixin(object):
69+
created = db.Column(
70+
db.DateTime, nullable=False, default=datetime.utcnow)
71+
updated = db.Column(db.DateTime, onupdate=datetime.utcnow)
6572

66-
db = SQLAlchemy(model_class=TimestampedModel)
73+
class Author(db.Model):
74+
...
6775

68-
class MyModel(db.Model):
76+
class Post(TimestampMixin, db.Model):
6977
...
7078

71-
All model classes extending from ``db.Model`` will now inherit a
72-
``created_at`` column.
7379

7480
Query Class
7581
-----------
7682

77-
It is also possible to customize what is availble for use on the
83+
It is also possible to customize what is available for use on the
7884
special ``query`` property of models. For example, providing a
7985
``get_or`` method::
8086

@@ -86,19 +92,15 @@ special ``query`` property of models. For example, providing a
8692

8793
db = SQLAlchemy(query_class=GetOrQuery)
8894

95+
# get a user by id, or return an anonymous user instance
96+
user = User.query.get_or(user_id, anonymous_user)
97+
8998
And now all queries executed from the special ``query`` property
9099
on Flask-SQLAlchemy models can use the ``get_or`` method as part
91100
of their queries. All relationships defined with
92-
``db.relationship`` (but not :func:`sqlalchemy.relationship`)
101+
``db.relationship`` (but not :func:`sqlalchemy.orm.relationship`)
93102
will also be provided with this functionality.
94103

95-
.. warning::
96-
97-
Unlike a custom ``Model`` base class, it is required
98-
to either inherit from either :class:`flask_sqlalchemy.BaseQuery`
99-
or :func:`sqlalchemy.orm.Query` in order to define a custom
100-
query class.
101-
102104
It also possible to define a custom query class for individual
103105
relationships as well, by providing the ``query_class`` keyword
104106
in the definition. This works with both ``db.relationship``
@@ -109,9 +111,8 @@ and ``sqlalchemy.relationship``::
109111

110112
.. note::
111113

112-
If a query class is defined on a relationship, it will take
113-
precedence over the query class attached to its corresponding
114-
model.
114+
If a query class is defined on a relationship, it will take precedence over
115+
the query class attached to its corresponding model.
115116

116117
It is also possible to define a specific query class for individual models
117118
by overriding the ``query_class`` class attribute on the model::
@@ -121,3 +122,69 @@ by overriding the ``query_class`` class attribute on the model::
121122

122123
In this case, the ``get_or`` method will be only availble on queries
123124
orginating from ``MyModel.query``.
125+
126+
127+
Model Metaclass
128+
---------------
129+
130+
.. warning::
131+
132+
Metaclasses are an advanced topic, and you probably don't need to customize
133+
them to achieve what you want. It is mainly documented here to show how to
134+
disable table name generation.
135+
136+
The model metaclass is responsible for setting up the SQLAlchemy internals when
137+
defining model subclasses. Flask-SQLAlchemy adds some extra behaviors through
138+
mixins; its default metaclass, :class:`~model.DefaultMeta`, inherits them all.
139+
140+
* :class:`~model.BindMetaMixin`: ``__bind_key__`` is extracted from the class
141+
and applied to the table. See :ref:`binds`.
142+
* :class:`~model.NameMetaMixin`: If the model does not specify a
143+
``__tablename__`` but does specify a primary key, a name is automatically
144+
generated.
145+
146+
You can add your own behaviors by defining your own metaclass and creating the
147+
declarative base yourself. Be sure to still inherit from the mixins you want
148+
(or just inherit from the default metaclass).
149+
150+
Passing a declarative base class instead of a simple model base class, as shown
151+
above, to ``base_class`` will cause Flask-SQLAlchemy to use this base instead
152+
of constructing one with the default metaclass. ::
153+
154+
from flask_sqlalchemy import SQLAlchemy
155+
from flask_sqlalchemy.model import DefaultMeta, Model
156+
157+
class CustomMeta(DefaultMeta):
158+
def __init__(cls, name, bases, d):
159+
# custom class setup could go here
160+
161+
# be sure to call super
162+
super(CustomMeta, cls).__init__(name, bases, d)
163+
164+
# custom class-only methods could go here
165+
166+
db = SQLAlchemy(model_class=declarative_base(
167+
cls=Model, metaclass=CustomMeta, name='Model'))
168+
169+
You can also pass whatever other arguments you want to
170+
:func:`~sqlalchemy.ext.declarative.declarative_base` to customize the base
171+
class as needed.
172+
173+
Disabling Table Name Generation
174+
```````````````````````````````
175+
176+
Some projects prefer to set each model's ``__tablename__`` manually rather than
177+
relying on Flask-SQLAlchemy's detection and generation. The table name
178+
generation can be disabled by defining a custom metaclass. ::
179+
180+
from flask_sqlalchemy.model import BindMetaMixin, Model
181+
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
182+
183+
class NoNameMeta(BindMetaMixin, DeclarativeMeta):
184+
pass
185+
186+
db = SQLAlchemy(model_class=declarative_base(
187+
cls=Model, metaclass=NoNameMeta, name='Model'))
188+
189+
This creates a base that still supports the ``__bind_key__`` feature but does
190+
not generate table names.

0 commit comments

Comments
 (0)