@@ -51,8 +51,6 @@ This strategy allows one to organize their views alongside the models/tables tho
5151views happen to be referencing, without requiring the view be importable at MetaData/model base
5252definition time.
5353
54- ### Option 1
55-
5654``` python
5755from sqlalchemy import Column, types, select
5856from sqlalchemy.ext.declarative import declarative_base
@@ -67,95 +65,43 @@ class Foo(Base):
6765 id = Column(types.Integer, primary_key = True )
6866
6967
70- @view ()
71- class Bar1 ( Base ) :
68+ @view (Base )
69+ class Bar :
7270 __tablename__ = ' bar'
7371 __view__ = select(Foo.__table__).where(Foo.__table__.id > 10 )
7472
7573 id = Column(types.Integer, primary_key = True )
7674```
7775
78- The primary difference between Options 1 and 2 in the above example is of how the
79- resulting classes are seen by SQLAlchemy/Alembic natively.
80-
81- In the case of ` Bar1 ` , SQLAlchemy/Alembic actually think that class is a normal table.
82- Therefore querying the view looks identical to a real table: ` session.query(Bar1).all() `
76+ The protocol this class is following provides an interface that is intentionally similar to the one
77+ given by a normal sqlalchemy model. From the perspective of code, your ` Bar ` class will be usable
78+ by SQLAlchemy in the same way as a normal table, i.e. ` session.query(Bar1).all() ` .
8379
84- For alembic, this means that alembic thinks you defined a table and will attempt to
85- autogenerate it (while this library will also notice it and attempt to autogenerate
86- a conflicting view .
80+ Alternatively, if you dont ** care ** about being able to programmatically make use of the model-like
81+ ORM interface, you can omit the model-style declaration of columns. That at least allows you to
82+ avoid duplicating the list of columns unnecessarily .
8783
88- In order to use this option, we suggest you use one or both of some utility functions provided
89- under the ` sqlalchemy_declarative_extensions.alembic ` : [ ignore_view_tables] ( sqlalchemy_declarative_extensions.alembic.ignore_view_tables )
90- and [ compose_include_object_callbacks] ( sqlalchemy_declarative_extensions.alembic.compose_include_object_callbacks ) .
84+ Finally, you can directly call ` register_view ` to imperitively register a normal [ View] ( sqlalchemy_declarative_extensions.View )
85+ object, if the class interface doesn't float your boat.
9186
92- Somewhere in your Alembic ` env.py ` , you will have a block which looks like this:
87+ ## Materialized views
9388
94- ``` python
95- with connectable.connect() as connection:
96- context.configure(
97- connection = connection,
98- target_metadata = target_metadata,
99- ...
100- )
101- ```
89+ Materialized views can be created by adding the ` materialized=True ` kwarg to the ` @view ` decorator,
90+ or else by supplying the same kwarg directly to the ` View ` constructor.
10291
103- The above call to ` configure ` accepts an ` include_object ` , which tells alembic to include or ignore
104- all detected objects.
92+ Note that in order to refresh materialized views concurrently, the Postgres requires the view to
93+ have a unique constraint. The constraint can be applied in the same way that it would be on a
94+ normal table (i.e. ` __table_args__ ` ):
10595
10696``` python
107- from sqlalchemy_declarative_extensions.alembic import ignore_view_tables
108- ...
109- context.configure(... , include_object = ignore_view_tables)
110- ```
111-
112- If you happen to already be using ` include_object ` to perform filtering, we provide an additional
113- utility to more easily compose our version with your own. Although you can certainly manually call
114- ` ignore_view_tables ` directly, yourself.
115-
116- ``` python
117- from sqlalchemy_declarative_extensions.alembic import ignore_view_tables, compose_include_object_callbacks
118- ...
119- def my_include_object (object , * _ ):
120- if object .name != ' foo' :
121- return True
122- return False
123-
124- context.configure(... , include_object = compose_include_object_callbacks(my_include_object, ignore_view_tables))
125- ```
126-
127- ## Option 2
128-
129- ``` python
130- from sqlalchemy import Column, types, select
131- from sqlalchemy.ext.declarative import declarative_base
132- from sqlalchemy_declarative_extensions import view
133-
134- Base = declarative_base()
135-
136-
137- class Foo (Base ):
138- __tablename__ = ' foo'
139-
140- id = Column(types.Integer, primary_key = True )
141-
142-
143- @view (Base) # or `@view(Base.metadata)`
144- class Bar2 :
97+ @view (Base, materialized = True )
98+ class Bar :
14599 __tablename__ = ' bar'
146100 __view__ = select(Foo.__table__).where(Foo.__table__.id > 10 )
101+ __table_args__ = (Index(' uq_bar' , ' id' , unique = True ))
147102```
148103
149- By contrast, with Option 2, your class is not subclassing ` Base ` , therefore it's
150- not registered as a real table by SQLAlchemy or Alembic. There's no additional
151- work required to get them to ignore the table, because it's not one.
152-
153- Unfortunately, that means you cannot ** invisibly** treat it as though it's a normal model,
154- largely because it doesn't have the columns enumerated out in the same way.
155-
156- However we can provide some basic support for treating it as a table as far as the ORM is concerned.
157- For example, you can still ` session.query(Bar2).all() ` directly.
158-
159- However, in most cases views primarily benefit non-code consumers of the database, because there's
160- no practical difference between querying a literal view, versus executing the underlying query
161- of that view, through something like ` session.execute(Bar2.__view__) ` .
104+ There is a caveat (at least currently), that the ` UniqueConstraint ` convenience class provided by
105+ SQLAlchemy does not appear to function in the same way as ` Index ` in a way that makes it incompatible
106+ with the mechanisms used by this library at the time of writing. As such, we ignore ` __table_args__ `
107+ constraints which are not ` Index ` .
0 commit comments