Advanced Customization¶
The various objects managed by the extension can be customized by passing arguments to
the SQLAlchemy
constructor.
Model Class¶
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
.
The following example gives every model an integer primary key, or a foreign key for joined-table inheritance.
Note
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.
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, declared_attr
class Base(DeclarativeBase):
@declared_attr.cascading
@classmethod
def id(cls):
for base in cls.__mro__[1:-1]:
if getattr(base, "__table__", None) is not None:
return mapped_column(ForeignKey(base.id), primary_key=True)
else:
return mapped_column(Integer, primary_key=True)
db = SQLAlchemy(app, model_class=Base)
class User(db.Model):
name: Mapped[str]
class Employee(User):
title: Mapped[str]
Abstract Models and Mixins¶
If behavior is only needed on some models rather than all models, use an abstract model base class to customize only those models. For example, if some models should track when they are created or updated.
from datetime import datetime, timezone
from sqlalchemy.orm import Mapped, mapped_column
class TimestampModel(db.Model):
__abstract__ = True
created: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
updated: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
class Author(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(unique=True)
class Post(TimestampModel):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
This can also be done with a mixin class, inheriting from db.Model
separately.
class TimestampMixin:
created: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
updated: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
class Post(TimestampMixin, db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
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 simple way to achieve that is to
set each __tablename__
and not modify the base class. However, the table name
generation can be disabled by setting disable_autonaming=True in the SQLAlchemy constructor.
class Base(sa_orm.DeclarativeBase):
pass
db = SQLAlchemy(app, model_class=Base, disable_autonaming=True)
Session Class¶
Flask-SQLAlchemy’s Session
class chooses which engine to query based on the
bind key associated with the model or table. However, there are other strategies such as
horizontal sharding that can be implemented with a different session class. The
class_
key to the session_options
argument to the extension to change the
session class.
Flask-SQLAlchemy will always pass the extension instance as the db
argument to the
session, so it must accept that to continue working. That can be used to get access to
db.engines
.
from sqlalchemy.ext.horizontal_shard import ShardedSession
from flask_sqlalchemy.session import Session
class CustomSession(ShardedSession, Session):
...
db = SQLAlchemy(session_options={"class_": CustomSession})
Query Class¶
Warning
The query interface is considered legacy in SQLAlchemy. This includes
session.query
, Model.query
, db.Query
, and lazy="dynamic"
relationships. Prefer using session.execute(select(...))
instead.
It is possible to customize the query interface used by the session, models, and
relationships. This can be used to add extra query methods. For example, you could add
a get_or
method that gets a row or returns a default.
from flask_sqlalchemy.query import Query
class GetOrQuery(Query):
def get_or(self, ident, default=None):
out = self.get(ident)
if out is None:
return default
return out
db = SQLAlchemy(query_class=GetOrQuery)
user = User.query.get_or(user_id, anonymous_user)
Passing the query_class
argument will customize db.Query
, db.session.query
,
Model.query
, and db.relationship(lazy="dynamic")
relationships. It’s also
possible to customize these on a per-object basis.
To customize a specific model’s query
property, set the query_class
attribute on
the model class.
class User(db.Model):
query_class = GetOrQuery
To customize a specific dynamic relationship, pass the query_class
argument to the
relationship.
db.relationship(User, lazy="dynamic", query_class=GetOrQuery)
To customize only session.query
, pass the query_cls
key to the
session_options
argument to the constructor.
db = SQLAlchemy(session_options={"query_cls": GetOrQuery})