Skip to content

Commit 1bf0ae7

Browse files
copelcomikicz
authored andcommitted
Support multiple databases (refs #9)
1 parent 269c17f commit 1bf0ae7

File tree

17 files changed

+318
-27
lines changed

17 files changed

+318
-27
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Virtualenvs for testing
44
.venv
55
venv/
6+
.direnv
7+
.envrc
68

79
# Temporary testing directories
810
test__*

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,25 @@ Provides args:
382382
* `sender` - Always `None`
383383

384384

385+
### Multiple databases
386+
387+
django-pgviews can use multiple databases. Similar to Django's `migrate`
388+
management command, our commands (`clear_pgviews`, `refresh_pgviews`,
389+
`sync_pgviews`) operate on one database at a time. You can specify which
390+
database to synchronize by providing the `--database` option. For example:
391+
392+
```shell
393+
python manage.py sync_pgviews # uses default db
394+
python manage.py sync_pgviews --database=myotherdb
395+
```
396+
397+
Unless using custom routers, django-pgviews will sync all views to the specified
398+
database. If you want to interact with multiple databases automatically, you'll
399+
need to take some additional steps. Please refer to Django's [Automatic database
400+
routing](https://docs.djangoproject.com/en/3.2/topics/db/multi-db/#automatic-database-routing)
401+
to pin views to specific databases.
402+
403+
385404
## Django Compatibility
386405

387406
<table>

django_pgviews/apps.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ViewConfig(apps.AppConfig):
1616
name = "django_pgviews"
1717
verbose_name = "Django Postgres Views"
1818

19-
def sync_pgviews(self, sender, app_config, **kwargs):
19+
def sync_pgviews(self, sender, app_config, using, **kwargs):
2020
"""
2121
Forcibly sync the views.
2222
"""
@@ -35,6 +35,7 @@ def sync_pgviews(self, sender, app_config, **kwargs):
3535
force=True,
3636
update=True,
3737
materialized_views_check_sql_changed=getattr(settings, "MATERIALIZED_VIEWS_CHECK_SQL_CHANGED", False),
38+
using=using,
3839
)
3940
self.counter = 0
4041

django_pgviews/management/commands/clear_pgviews.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from django.apps import apps
44
from django.core.management.base import BaseCommand
5-
from django.db import connection
5+
from django.db import DEFAULT_DB_ALIAS
66

77
from django_pgviews.view import clear_view, View, MaterializedView
88

@@ -12,11 +12,21 @@
1212
class Command(BaseCommand):
1313
help = """Clear Postgres views. Use this before running a migration"""
1414

15-
def handle(self, **options):
15+
def add_arguments(self, parser):
16+
parser.add_argument(
17+
"--database",
18+
default=DEFAULT_DB_ALIAS,
19+
help='Nominates a database to synchronize. Defaults to the "default" database.',
20+
)
21+
22+
def handle(self, database, **options):
1623
for view_cls in apps.get_models():
1724
if not (isinstance(view_cls, type) and issubclass(view_cls, View) and hasattr(view_cls, "sql")):
1825
continue
1926
python_name = "{}.{}".format(view_cls._meta.app_label, view_cls.__name__)
27+
connection = view_cls.get_view_connection(using=database)
28+
if not connection:
29+
continue
2030
status = clear_view(
2131
connection, view_cls._meta.db_table, materialized=isinstance(view_cls(), MaterializedView)
2232
)

django_pgviews/management/commands/refresh_pgviews.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.core.management.base import BaseCommand
2+
from django.db import DEFAULT_DB_ALIAS
23

34
from django_pgviews.models import ViewRefresher
45

@@ -14,6 +15,11 @@ def add_arguments(self, parser):
1415
dest="concurrently",
1516
help="Refresh concurrently if the materialized view supports it",
1617
)
18+
parser.add_argument(
19+
"--database",
20+
default=DEFAULT_DB_ALIAS,
21+
help='Nominates a database to synchronize. Defaults to the "default" database.',
22+
)
1723

18-
def handle(self, concurrently, **options):
19-
ViewRefresher().run(concurrently)
24+
def handle(self, concurrently, database, **options):
25+
ViewRefresher().run(concurrently, using=database)

django_pgviews/management/commands/sync_pgviews.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.conf import settings
22
from django.core.management.base import BaseCommand
3+
from django.db import DEFAULT_DB_ALIAS
34

45
from django_pgviews.models import ViewSyncer
56

@@ -46,11 +47,16 @@ def add_arguments(self, parser):
4647
"if the SQL is different. By default uses django setting MATERIALIZED_VIEWS_CHECK_SQL_CHANGED."
4748
),
4849
)
50+
parser.add_argument(
51+
"--database",
52+
default=DEFAULT_DB_ALIAS,
53+
help='Nominates a database to synchronize. Defaults to the "default" database.',
54+
)
4955

50-
def handle(self, force, update, materialized_views_check_sql_changed, **options):
56+
def handle(self, force, update, materialized_views_check_sql_changed, database, **options):
5157
vs = ViewSyncer()
5258

5359
if materialized_views_check_sql_changed is None:
5460
materialized_views_check_sql_changed = getattr(settings, "MATERIALIZED_VIEWS_CHECK_SQL_CHANGED", False)
5561

56-
vs.run(force, update, materialized_views_check_sql_changed=materialized_views_check_sql_changed)
62+
vs.run(force, update, using=database, materialized_views_check_sql_changed=materialized_views_check_sql_changed)

django_pgviews/models.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22

33
from django.apps import apps
4-
from django.db import connection
54

65
from django_pgviews.signals import view_synced, all_views_synced
76
from django_pgviews.view import create_view, View, MaterializedView, create_materialized_view
@@ -38,13 +37,16 @@ def run_backlog(self, backlog, **kwargs):
3837

3938

4039
class ViewSyncer(RunBacklog):
41-
def run(self, force, update, materialized_views_check_sql_changed=False, **options):
40+
def run(self, force, update, using, materialized_views_check_sql_changed=False, **options):
4241
if super().run(
43-
force=force, update=update, materialized_views_check_sql_changed=materialized_views_check_sql_changed
42+
force=force,
43+
update=update,
44+
using=using,
45+
materialized_views_check_sql_changed=materialized_views_check_sql_changed,
4446
):
45-
all_views_synced.send(sender=None)
47+
all_views_synced.send(sender=None, using=using)
4648

47-
def run_backlog(self, backlog, *, force, update, materialized_views_check_sql_changed, **kwargs):
49+
def run_backlog(self, backlog, *, force, update, using, materialized_views_check_sql_changed, **kwargs):
4850
"""Installs the list of models given from the previous backlog
4951
5052
If the correct dependent views have not been installed, the view
@@ -67,6 +69,10 @@ def run_backlog(self, backlog, *, force, update, materialized_views_check_sql_ch
6769
continue # Skip
6870

6971
try:
72+
connection = view_cls.get_view_connection(using=using)
73+
if not connection:
74+
logger.info("Skipping pgview %s (migrations not allowed on %s)", name, using)
75+
continue # Skip
7076
if isinstance(view_cls(), MaterializedView):
7177
status = create_materialized_view(
7278
connection, view_cls, check_sql_changed=materialized_views_check_sql_changed
@@ -86,6 +92,7 @@ def run_backlog(self, backlog, *, force, update, materialized_views_check_sql_ch
8692
force=force,
8793
status=status,
8894
has_changed=status not in ("EXISTS", "FORCE_REQUIRED"),
95+
using=using,
8996
)
9097
self.finished.append(name)
9198
except Exception as exc:
@@ -114,10 +121,10 @@ def run_backlog(self, backlog, *, force, update, materialized_views_check_sql_ch
114121

115122

116123
class ViewRefresher(RunBacklog):
117-
def run(self, concurrently, **kwargs):
118-
return super().run(concurrently=concurrently, **kwargs)
124+
def run(self, concurrently, using, **kwargs):
125+
return super().run(concurrently=concurrently, using=using, **kwargs)
119126

120-
def run_backlog(self, backlog, *, concurrently, **kwargs):
127+
def run_backlog(self, backlog, *, concurrently, using, **kwargs):
121128
new_backlog = []
122129
for view_cls in backlog:
123130
skip = False
@@ -132,6 +139,11 @@ def run_backlog(self, backlog, *, concurrently, **kwargs):
132139
logger.info("Putting pgview at back of queue: %s", name)
133140
continue # Skip
134141

142+
# Don't refresh views not associated with this database
143+
connection = view_cls.get_view_connection(using=using)
144+
if not connection:
145+
continue
146+
135147
if issubclass(view_cls, MaterializedView):
136148
view_cls.refresh(concurrently=concurrently)
137149
logger.info("pgview %s refreshed", name)

django_pgviews/signals.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.dispatch import Signal
22

33

4-
view_synced = Signal(providing_args=["update", "force", "status", "has_changed"])
5-
all_views_synced = Signal()
4+
view_synced = Signal(providing_args=["update", "force", "status", "has_changed", "using"])
5+
all_views_synced = Signal(providing_args=["using"])

django_pgviews/view.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import psycopg2
1010
from django.apps import apps
1111
from django.core import exceptions
12-
from django.db import connection, transaction
12+
from django.db import connections, router, transaction
1313
from django.db import models
1414
from django.db.models.query import QuerySet
1515

@@ -371,6 +371,18 @@ class View(models.Model, metaclass=ViewMeta):
371371
def get_sql(cls):
372372
return ViewSQL(cls.sql, None)
373373

374+
@classmethod
375+
def get_view_connection(cls, using):
376+
"""
377+
Returns connection for "using" database if migrations are allowed (via
378+
router). Returns None if migrations are not allowed to indicate view
379+
should not be used on the specified database.
380+
381+
Overwrite this method in subclass to customize, if needed.
382+
"""
383+
if router.allow_migrate(using, cls._meta.app_label):
384+
return connections[using]
385+
374386
class Meta:
375387
abstract = True
376388
managed = False
@@ -438,7 +450,11 @@ class MaterializedView(View):
438450

439451
@classmethod
440452
def refresh(self, concurrently=False):
441-
cursor_wrapper = connection.cursor()
453+
conn = self.get_view_connection(using=router.db_for_write(self))
454+
if not conn:
455+
logger.warning("Failed to find connection to refresh %s", self)
456+
return
457+
cursor_wrapper = conn.cursor()
442458
cursor = cursor_wrapper.cursor
443459
try:
444460
if self._concurrent_index is not None and concurrently:

tests/test_project/test_project/multidbtest/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)