Views#
Views definition and registration can be performed exactly as it is done with
other object types, by defining the set of views on the MetaData
or
declarative base, like so:
from sqlalchemy.orm import declarative_base
from sqlalchemy_declarative_extensions import declarative_database, View, Views
_Base = declarative_base()
@declarative_database
class Base(_Base):
__abstract__ = True
views = Views().are(
View("foo", "select * from bar where id > 10", schema="baz"),
)
And if you want to define views using raw strings, or otherwise not reference
the tables produced off the MetaData
, then this is absolutely a valid way to
organize.
The view
decorator#
However views differ from most of the other object types, in that they are convenient to define in terms of the tables they reference (i.e. your existing set of models/tables). In fact personally, all of my views are produced from select expressions referencing the underlying Table object.
This commonly introduce a circular reference problem wherein your tables/models are defined through subclassing the declarative base, which means your declarative base cannot then have the views statically defined on the base (while simultaneously referencing those models).
Note
There are ways of working around this in SQLAlchemy-land. For example by creating a MetaData
ahead of time and defining all models in terms of their underlying Table
.
Or perhaps by using SQLAlchemy’s mapper apis such that you’re not subclassing the declarative base for models.
In any case, these options are more complex and probably atypical. As such, we cannot assume you will adopt them.
For everyone else, the view decorator is meant to be the solution to that problem.
This strategy allows one to organize their views alongside the models/tables those views happen to be referencing, without requiring the view be importable at MetaData/model base definition time.
from sqlalchemy import Column, types, select
from sqlalchemy.orm import declarative_base
from sqlalchemy_declarative_extensions import view
Base = declarative_base()
class Foo(Base):
__tablename__ = 'foo'
id = Column(types.Integer, primary_key=True)
# Gets support for `session.query(Bar)...`
@view(Base, register_as_model=True)
class Bar:
__tablename__ = 'bar'
__view__ = select(Foo.__table__).where(Foo.__table__.id > 10)
id = Column(types.Integer, primary_key=True)
# Can still `session.execute(Baz.__view__)`, but does **not** support `session.query(Baz)...`
@view(Base)
class Baz:
__tablename__ = 'bar'
__view__ = select(Foo.__table__).where(Foo.__table__.id > 10)
The protocol this class is following provides an interface that is intentionally
similar to the one given by a normal sqlalchemy model. From the perspective of
code, your Bar
class will be usable by SQLAlchemy in the same way as a normal
table, i.e. session.query(Bar1).all()
.
Alternatively, if you dont care about being able to programmatically make
use of the model-like ORM interface, you can omit the model-style declaration of
columns (and the corresponding register_as_model=True
argument). That at least
allows you to avoid duplicating the list of columns unnecessarily.
Finally, you can directly call register_view
to imperitively register a normal
View
object, if the class interface doesn’t float your boat.
Materialized views#
Materialized views can be created by adding the materialized=True
kwarg to the
@view
decorator, or else by supplying the same kwarg directly to the View
constructor.
Note that in order to refresh materialized views concurrently, the Postgres
requires the view to have a unique constraint. The constraint can be applied in
the same way that it would be on a normal table (i.e. __table_args__
):
@view(Base, materialized=True)
class Bar:
__tablename__ = 'bar'
__view__ = select(Foo.__table__).where(Foo.__table__.id > 10)
__table_args__ = (Index('uq_bar', 'id', unique=True))
Additionally the sqlalchemy UniqueConstraint
index type is supported.
Internally these options are converted to
sqlalchemy_declarative_extensions.ViewIndex
, which you can instead use
directly, if desired.