|
| 1 | +.. include:: ./snippets/postgres_doc_links.rst |
| 2 | + |
| 3 | +.. _views_page: |
| 4 | + |
| 5 | + |
| 6 | +Views & materialized views |
| 7 | +========================== |
| 8 | + |
| 9 | +:class:`~psqlextra.models.PostgresViewModel` and :class:`~psqlextra.models.PostgresMaterializedViewModel` add support for `PostgreSQL Views`_ and `PostgreSQL Materialized Views`_. |
| 10 | + |
| 11 | +.. note:: |
| 12 | + |
| 13 | + You can create indices and constraints on (materialized) views just like you would on normal PostgreSQL tables. This is fully supported. |
| 14 | + |
| 15 | + |
| 16 | +Known limitations |
| 17 | +----------------- |
| 18 | + |
| 19 | +Changing view query |
| 20 | +******************* |
| 21 | + |
| 22 | +THere is **NO SUPPORT** whatsoever for changing the backing query of a view after the initial creation. |
| 23 | + |
| 24 | +Such changes are not detected by ``python manage.py pgmakemigrations`` and there are no pre-built operations for modifying them. |
| 25 | + |
| 26 | + |
| 27 | +Creating a (materialized) view |
| 28 | +------------------------------ |
| 29 | + |
| 30 | +Views are declared like regular Django models with a special base class and an extra option to specify the query backing the view. Once declared, they behave like regular Django models with the exception that you cannot write to them. |
| 31 | + |
| 32 | +Declaring the model |
| 33 | +******************* |
| 34 | + |
| 35 | +.. warning:: |
| 36 | + |
| 37 | + All fields returned by the backing query must be declared as Django fields. Fields that are returned by the query that aren't declared as Django fields become |
| 38 | + part of the view, but will not be visible from Django. |
| 39 | + |
| 40 | +With a queryset |
| 41 | +~~~~~~~~~~~~~~~ |
| 42 | + |
| 43 | +.. code-block:: python |
| 44 | +
|
| 45 | + from django.db import models |
| 46 | +
|
| 47 | + from psqlextra.models import PostgresViewModel |
| 48 | +
|
| 49 | +
|
| 50 | + class MyView(PostgresViewModel): |
| 51 | + name = models.TextField() |
| 52 | + somefk__name = models.TextField() |
| 53 | +
|
| 54 | + class Meta: |
| 55 | + indexes = [models.Index(fields=["name"])] |
| 56 | +
|
| 57 | + class ViewMeta: |
| 58 | + query = SomeOtherModel.objects.values('id', 'name', 'somefk__name') |
| 59 | +
|
| 60 | + class MyMaterializedView(PostgresMaterializedViewModel): |
| 61 | + name = models.TextField() |
| 62 | + somefk__name = models.TextField() |
| 63 | +
|
| 64 | + class Meta: |
| 65 | + indexes = [models.Index(fields=["name"])] |
| 66 | +
|
| 67 | + class ViewMeta: |
| 68 | + query = SomeOtherModel.objects.values('id', 'name', 'somefk__name') |
| 69 | +
|
| 70 | +With raw SQL |
| 71 | +~~~~~~~~~~~~ |
| 72 | + |
| 73 | +Any raw SQL can be used as the backing query for a view. Specify a tuple to pass the values for placeholders. |
| 74 | + |
| 75 | +.. code-block:: python |
| 76 | +
|
| 77 | + from django.db import models |
| 78 | +
|
| 79 | + from psqlextra.models import PostgresViewModel |
| 80 | +
|
| 81 | +
|
| 82 | + class MyView(PostgresViewModel): |
| 83 | + name = models.TextField() |
| 84 | + somefk__name = models.TextField() |
| 85 | +
|
| 86 | + class Meta: |
| 87 | + indexes = [models.Index(fields=["name"])] |
| 88 | +
|
| 89 | + class ViewMeta: |
| 90 | + query = "SELECT id, somefk.name AS somefk__name FROM mytable INNER JOIN somefk ON somefk.id = mytable.somefk_id" |
| 91 | +
|
| 92 | + class MyMaterializedView(PostgresMaterializedViewModel): |
| 93 | + name = models.TextField() |
| 94 | + somefk__name = models.TextField() |
| 95 | +
|
| 96 | + class Meta: |
| 97 | + indexes = [models.Index(fields=["name"])] |
| 98 | +
|
| 99 | + class ViewMeta: |
| 100 | + query = ("SELECT id, somefk.name AS somefk__name FROM mytable INNER JOIN somefk ON somefk.id = mytable.somefk_id WHERE id > %s", 1) |
| 101 | +
|
| 102 | +
|
| 103 | +With a callable |
| 104 | +~~~~~~~~~~~~~~~ |
| 105 | + |
| 106 | +A callable can be used when your query depends on settings or other variables that aren't available at evaluation time. The callable can return raw SQL, raw SQL with params or a queryset. |
| 107 | + |
| 108 | +.. code-block:: python |
| 109 | +
|
| 110 | + from django.db import models |
| 111 | +
|
| 112 | + from psqlextra.models import PostgresViewModel |
| 113 | +
|
| 114 | + def _generate_query(): |
| 115 | + return ("SELECT * FROM sometable WHERE app_name = %s", settings.APP_NAME) |
| 116 | +
|
| 117 | + def _build_query(): |
| 118 | + return SomeTable.objects.filter(app_name=settings.APP_NAME) |
| 119 | +
|
| 120 | +
|
| 121 | + class MyView(PostgresViewModel): |
| 122 | + name = models.TextField() |
| 123 | + somefk__name = models.TextField() |
| 124 | +
|
| 125 | + class ViewMeta: |
| 126 | + query = _generate_query |
| 127 | +
|
| 128 | + class MyMaterializedView(PostgresMaterializedViewModel): |
| 129 | + name = models.TextField() |
| 130 | + somefk__name = models.TextField() |
| 131 | +
|
| 132 | + class ViewMeta: |
| 133 | + query = _generate_query |
| 134 | +
|
| 135 | +
|
| 136 | +Generating a migration |
| 137 | +********************** |
| 138 | +Run the following command to automatically generate a migration: |
| 139 | + |
| 140 | +.. code-block:: bash |
| 141 | +
|
| 142 | + python manage.py pgmakemigrations |
| 143 | +
|
| 144 | +This will generate a migration that creates the view with the specified query as the base. |
| 145 | + |
| 146 | +.. warning:: |
| 147 | + |
| 148 | + Always use ``python manage.py pgmakemigrations`` for view models. |
| 149 | + |
| 150 | + The model must be created by the :class:`~psqlextra.backend.migrations.operations.PostgresCreateViewModel` or :class:`~psqlextra.backend.migrations.operations.PostgresCreateMaterializedViewModel` operation. |
| 151 | + |
| 152 | + Do not use the standard ``python manage.py makemigrations`` command for view models. Django will issue a standard :class:`~django:django.db.migrations.operations.CreateModel` operation. Doing this will not create a view and all subsequent operations will fail. |
| 153 | + |
| 154 | + |
| 155 | +Refreshing a materialized view |
| 156 | +------------------------------ |
| 157 | + |
| 158 | +Make sure to read the PostgreSQL documentation on refreshing materialized views for caveats: `PostgreSQL Refresh Materialized Views`_. |
| 159 | + |
| 160 | +.. code-block:: python |
| 161 | +
|
| 162 | + # Takes an AccessExclusive lock and blocks till table is re-filled |
| 163 | + MyViewModel.refresh() |
| 164 | +
|
| 165 | + # Allows concurrent read, does block till table is re-filled. |
| 166 | + # Warning: Only works if the view was refreshed at least once before. |
| 167 | + MyViewModel.refresh(concurrently=True) |
| 168 | +
|
| 169 | +
|
| 170 | +Creating a materialized view without data |
| 171 | +----------------------------------------- |
| 172 | + |
| 173 | +.. warning:: |
| 174 | + |
| 175 | + You cannot query your materialized view until it has been refreshed at least once. After creating the materialized view without data, you must execute a refresh at some point. The first refresh cannot be ``CONCURRENTLY`` (PostgreSQL restriction). |
| 176 | + |
| 177 | +By default, the migration creates the materialized view and executes the first refresh. If you want to avoid this, pass the ``with_data=False`` flag in the :class:`~psqlextra.backend.migrations.operations.PostgresCreateMaterializedViewModel` operation in your generated migration. |
| 178 | + |
| 179 | +.. code-block:: python |
| 180 | +
|
| 181 | + from django.db import migrations, models |
| 182 | +
|
| 183 | + from psqlextra.backend.migrations.operations import PostgresCreateMaterializedViewModel |
| 184 | +
|
| 185 | + class Migration(migrations.Migration): |
| 186 | + operations = [ |
| 187 | + PostgresCreateMaterializedViewModel( |
| 188 | + name="myview", |
| 189 | + fields=[...], |
| 190 | + options={...}, |
| 191 | + view_options={ |
| 192 | + "query": ... |
| 193 | + }, |
| 194 | + # Not the default, creates materialized with `WITH NO DATA` |
| 195 | + with_data=False, |
| 196 | + ) |
| 197 | + ] |
0 commit comments