Skip to content

Commit

Permalink
Merge pull request #546 from davidism/metaclass
Browse files Browse the repository at this point in the history
allow passing declarative_base as model_class
  • Loading branch information
davidism authored Sep 28, 2017
2 parents a04b971 + 9aa42b2 commit fa8f9bf
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 181 deletions.
27 changes: 27 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
Changelog
=========

Version 2.3.0
-------------

In development

- Multiple bugs with ``__tablename__`` generation are fixed. Names will be
generated for models that define a primary key, but not for single-table
inheritance subclasses. Names will not override a ``declared_attr``.
``PrimaryKeyConstraint`` is detected. (`#541`_)
- Passing an existing ``declarative_base()`` as ``model_class`` to
``SQLAlchemy.__init__`` will use this as the base class instead of creating
one. This allows customizing the metaclass used to construct the base.
(`#546`_)
- The undocumented ``DeclarativeMeta`` internals that the extension uses for
binds and table name generation have been refactored to work as mixins.
Documentation is added about how to create a custom metaclass that does not
do table name generation. (`#546`_)
- Model and metaclass code has been moved to a new ``models`` module.
``_BoundDeclarativeMeta`` is renamed to ``DefaultMeta``; the old name will be
removed in 3.0. (`#546`_)
- Models have a default ``repr`` that shows the model name and primary key.
(`#530`_)

.. _#530: https://github.com/mitsuhiko/flask-sqlalchemy/pull/530
.. _#541: https://github.com/mitsuhiko/flask-sqlalchemy/pull/541
.. _#546: https://github.com/mitsuhiko/flask-sqlalchemy/pull/546

Version 2.2
-----------

Expand Down
27 changes: 12 additions & 15 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ API

.. module:: flask_sqlalchemy

This part of the documentation documents all the public classes and
functions in Flask-SQLAlchemy.

Configuration
`````````````

Expand All @@ -16,33 +13,33 @@ Models
``````

.. autoclass:: Model
:members:
:members:

.. attribute:: __bind_key__
.. attribute:: __bind_key__

Optionally declares the bind to use. `None` refers to the default
bind. For more information see :ref:`binds`.
Optionally declares the bind to use. ``None`` refers to the default
bind. For more information see :ref:`binds`.

.. attribute:: __tablename__
.. attribute:: __tablename__

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

.. autoclass:: BaseQuery
:members:
:members:

Sessions
````````

.. autoclass:: SignallingSession
:members:
:members:

Utilities
`````````

.. autoclass:: Pagination
:members:
:members:

.. autofunction:: get_debug_queries
167 changes: 117 additions & 50 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,75 +6,81 @@ Customizing
===========

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

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


Model Class
-----------

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

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

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

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

class MyModel(db.Model):
...
::

.. note::
from flask_sqlalchemy import Model, SQLAlchemy
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr, has_inherited_table

While not strictly necessary to inherit from :class:`flask_sqlalchemy.Model`
it is encouraged as future changes may cause incompatibility.
class IdModel(Model):
@declared_attr
def id(cls):
for base in cls.__mro__[1:-1]:
if getattr(base, '__table__', None) is not None:
type = sa.ForeignKey(base.id)
break
else:
type = sa.Integer

.. note::
return sa.Column(type, primary_key=True)

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

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

class ReprBase(object):
def __repr__(self):
return "<{0} id: {1}>".format(self.__class__.__name__, self.id)
class Employee(User):
title = db.Column(db.String)

db = SQLAlchemy()

class MyModel(db.Model, ReprBase):
...
Model Mixins
------------

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

from flask_sqlalchemy import Model, SQLAlchemy
from sqlalchemy import Column, DateTime
from datetime import datetime

class TimestampedModel(Model):
created_at = Column(DateTime, default=datetime.utcnow)
class TimestampMixin(object):
created = db.Column(
db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow)

db = SQLAlchemy(model_class=TimestampedModel)
class Author(db.Model):
...

class MyModel(db.Model):
class Post(TimestampMixin, db.Model):
...

All model classes extending from ``db.Model`` will now inherit a
``created_at`` column.

Query Class
-----------

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

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

db = SQLAlchemy(query_class=GetOrQuery)

# get a user by id, or return an anonymous user instance
user = User.query.get_or(user_id, anonymous_user)

And now all queries executed from the special ``query`` property
on Flask-SQLAlchemy models can use the ``get_or`` method as part
of their queries. All relationships defined with
``db.relationship`` (but not :func:`sqlalchemy.relationship`)
``db.relationship`` (but not :func:`sqlalchemy.orm.relationship`)
will also be provided with this functionality.

.. warning::

Unlike a custom ``Model`` base class, it is required
to either inherit from either :class:`flask_sqlalchemy.BaseQuery`
or :func:`sqlalchemy.orm.Query` in order to define a custom
query class.

It also possible to define a custom query class for individual
relationships as well, by providing the ``query_class`` keyword
in the definition. This works with both ``db.relationship``
Expand All @@ -109,9 +111,8 @@ and ``sqlalchemy.relationship``::

.. note::

If a query class is defined on a relationship, it will take
precedence over the query class attached to its corresponding
model.
If a query class is defined on a relationship, it will take precedence over
the query class attached to its corresponding model.

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

In this case, the ``get_or`` method will be only availble on queries
orginating from ``MyModel.query``.


Model Metaclass
---------------

.. warning::

Metaclasses are an advanced topic, and you probably don't need to customize
them to achieve what you want. It is mainly documented here to show how to
disable table name generation.

The model metaclass is responsible for setting up the SQLAlchemy internals when
defining model subclasses. Flask-SQLAlchemy adds some extra behaviors through
mixins; its default metaclass, :class:`~model.DefaultMeta`, inherits them all.

* :class:`~model.BindMetaMixin`: ``__bind_key__`` is extracted from the class
and applied to the table. See :ref:`binds`.
* :class:`~model.NameMetaMixin`: If the model does not specify a
``__tablename__`` but does specify a primary key, a name is automatically
generated.

You can add your own behaviors by defining your own metaclass and creating the
declarative base yourself. Be sure to still inherit from the mixins you want
(or just inherit from the default metaclass).

Passing a declarative base class instead of a simple model base class, as shown
above, to ``base_class`` will cause Flask-SQLAlchemy to use this base instead
of constructing one with the default metaclass. ::

from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.model import DefaultMeta, Model

class CustomMeta(DefaultMeta):
def __init__(cls, name, bases, d):
# custom class setup could go here

# be sure to call super
super(CustomMeta, cls).__init__(name, bases, d)

# custom class-only methods could go here

db = SQLAlchemy(model_class=declarative_base(
cls=Model, metaclass=CustomMeta, name='Model'))

You can also pass whatever other arguments you want to
:func:`~sqlalchemy.ext.declarative.declarative_base` to customize the base
class as needed.

Disabling Table Name Generation
```````````````````````````````

Some projects prefer to set each model's ``__tablename__`` manually rather than
relying on Flask-SQLAlchemy's detection and generation. The table name
generation can be disabled by defining a custom metaclass. ::

from flask_sqlalchemy.model import BindMetaMixin, Model
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base

class NoNameMeta(BindMetaMixin, DeclarativeMeta):
pass

db = SQLAlchemy(model_class=declarative_base(
cls=Model, metaclass=NoNameMeta, name='Model'))

This creates a base that still supports the ``__bind_key__`` feature but does
not generate table names.
Loading

0 comments on commit fa8f9bf

Please sign in to comment.