Skip to content

Commit f928c81

Browse files
dopatramanRoss Mechanic
authored andcommitted
Allow the use of multiple databases (#539)
* create historical record in the table associated with the historical record's table * cleanup * override using argument * format * docs * tests * multi db support * fixed conflicts * test-only class needs Meta attribute for migrations to apply to separate database * updated docs with multiple db testing information * more tests to ensure that queries on historical models in separate dbs are still possible * use_base_model_db * Fixed tests * updated docs with use_base_model_db
1 parent 6b113e3 commit f928c81

File tree

7 files changed

+123
-7
lines changed

7 files changed

+123
-7
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Authors
7070
- Nianpeng Li
7171
- Nick Träger
7272
- Phillip Marshall
73+
- Prakash Venkatraman (`dopatraman <https://github.com/dopatraman>`_)
7374
- Rajesh Pappula
7475
- Ray Logel
7576
- Roberto Aguilar

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Unreleased
88
----------
99
- Added the possibility to create a relation to the original model
1010

11+
2.7.1 (2019-03-26)
12+
------------------
13+
- Added routing for saving historical records to separate databases if necessary.
14+
1115
2.7.0 (2019-01-16)
1216
------------------
1317
- Add support for ``using`` chained manager method and save/delete keyword argument (gh-507)

docs/multiple_dbs.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,18 @@ an issue where you want to track the history on a table that lives in a separate
3939
database to your user model. Since Django does not support cross-database relations,
4040
you will have to manually track the ``history_user`` using an explicit ID. The full
4141
documentation on this feature is in :ref:`Manually Track User Model`.
42+
43+
Tracking History Separate from the Base Model
44+
---------------------------------------------
45+
You can choose whether or not to track models' history in the same database by
46+
setting the flag `use_base_model_db`.
47+
48+
```
49+
class MyModel(models.Model):
50+
...
51+
history = HistoricalRecords(use_base_model_db=False)
52+
```
53+
54+
If set to `True`, migrations and audit
55+
events will be sent to the same database as the base model. If `False`, they
56+
will be sent to the place specified by the database router.

simple_history/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def __init__(
7979
history_user_getter=_history_user_getter,
8080
history_user_setter=_history_user_setter,
8181
related_name=None,
82+
use_base_model_db=True,
8283
):
8384
self.user_set_verbose_name = verbose_name
8485
self.user_related_name = user_related_name
@@ -95,6 +96,7 @@ def __init__(
9596
self.user_getter = history_user_getter
9697
self.user_setter = history_user_setter
9798
self.related_name = related_name
99+
self.use_base_model_db = use_base_model_db
98100

99101
if excluded_fields is None:
100102
excluded_fields = []
@@ -433,14 +435,20 @@ def post_save(self, instance, created, using=None, **kwargs):
433435
if not created and hasattr(instance, "skip_history_when_saving"):
434436
return
435437
if not kwargs.get("raw", False):
436-
self.create_historical_record(instance, created and "+" or "~", using=using)
438+
self.create_historical_record(
439+
instance,
440+
created and "+" or "~",
441+
using=using if self.use_base_model_db else None,
442+
)
437443

438444
def post_delete(self, instance, using=None, **kwargs):
439445
if self.cascade_delete_history:
440446
manager = getattr(instance, self.manager_name)
441447
manager.using(using).all().delete()
442448
else:
443-
self.create_historical_record(instance, "-", using=using)
449+
self.create_historical_record(
450+
instance, "-", using=using if self.use_base_model_db else None
451+
)
444452

445453
def create_historical_record(self, instance, history_type, using=None):
446454
history_date = getattr(instance, "_history_date", now())

simple_history/tests/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ class ModelWithHistoryInDifferentApp(models.Model):
362362
history = HistoricalRecords(app="external")
363363

364364

365+
class ModelWithHistoryInDifferentDb(models.Model):
366+
name = models.CharField(max_length=30)
367+
history = HistoricalRecords(use_base_model_db=False)
368+
369+
365370
###############################################################################
366371
#
367372
# Inheritance examples

simple_history/tests/tests/test_models.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
from simple_history import register
1919
from simple_history.exceptions import RelatedNameConflictError
2020
from simple_history.models import HistoricalRecords, ModelChange
21-
from simple_history.signals import pre_create_historical_record
21+
from simple_history.signals import (
22+
pre_create_historical_record,
23+
post_create_historical_record,
24+
)
2225
from simple_history.tests.custom_user.models import CustomUser
2326
from simple_history.tests.tests.utils import (
2427
database_router_override_settings,
2528
middleware_override_settings,
29+
database_router_override_settings_history_in_diff_db,
2630
)
2731
from simple_history.utils import get_history_model_for_model
2832
from simple_history.utils import update_change_reason
@@ -62,6 +66,7 @@
6266
HistoricalPollWithHistoricalIPAddress,
6367
HistoricalState,
6468
Library,
69+
ModelWithHistoryInDifferentDb,
6570
MultiOneToOne,
6671
Person,
6772
Place,
@@ -1478,3 +1483,46 @@ def test_revert(self):
14781483

14791484
self.one = Street.objects.get(pk=id)
14801485
self.assertEqual(self.one.history.count(), 4)
1486+
1487+
1488+
@override_settings(**database_router_override_settings_history_in_diff_db)
1489+
class SaveHistoryInSeparateDatabaseTestCase(TestCase):
1490+
multi_db = True
1491+
1492+
def setUp(self):
1493+
self.model = ModelWithHistoryInDifferentDb.objects.create(name="test")
1494+
1495+
def test_history_model_saved_in_separate_db(self):
1496+
self.assertEqual(0, self.model.history.using("default").count())
1497+
self.assertEqual(1, self.model.history.count())
1498+
self.assertEqual(1, self.model.history.using("other").count())
1499+
self.assertEqual(
1500+
1, ModelWithHistoryInDifferentDb.objects.using("default").count()
1501+
)
1502+
self.assertEqual(1, ModelWithHistoryInDifferentDb.objects.count())
1503+
self.assertEqual(
1504+
0, ModelWithHistoryInDifferentDb.objects.using("other").count()
1505+
)
1506+
1507+
def test_history_model_saved_in_separate_db_on_delete(self):
1508+
id = self.model.id
1509+
self.model.delete()
1510+
1511+
self.assertEqual(
1512+
0,
1513+
ModelWithHistoryInDifferentDb.history.using("default")
1514+
.filter(id=id)
1515+
.count(),
1516+
)
1517+
self.assertEqual(2, ModelWithHistoryInDifferentDb.history.filter(id=id).count())
1518+
self.assertEqual(
1519+
2,
1520+
ModelWithHistoryInDifferentDb.history.using("other").filter(id=id).count(),
1521+
)
1522+
self.assertEqual(
1523+
0, ModelWithHistoryInDifferentDb.objects.using("default").count()
1524+
)
1525+
self.assertEqual(0, ModelWithHistoryInDifferentDb.objects.count())
1526+
self.assertEqual(
1527+
0, ModelWithHistoryInDifferentDb.objects.using("other").count()
1528+
)

simple_history/tests/tests/utils.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import django
22
from django.conf import settings
33

4+
from simple_history.tests.models import HistoricalModelWithHistoryInDifferentDb
5+
46
request_middleware = "simple_history.middleware.HistoryRequestMiddleware"
57

8+
OTHER_DB_NAME = "other"
9+
610
if django.__version__ >= "2.0":
711
middleware_override_settings = {
812
"MIDDLEWARE": (settings.MIDDLEWARE + [request_middleware])
@@ -16,12 +20,12 @@
1620
class TestDbRouter(object):
1721
def db_for_read(self, model, **hints):
1822
if model._meta.app_label == "external":
19-
return "other"
23+
return OTHER_DB_NAME
2024
return None
2125

2226
def db_for_write(self, model, **hints):
2327
if model._meta.app_label == "external":
24-
return "other"
28+
return OTHER_DB_NAME
2529
return None
2630

2731
def allow_relation(self, obj1, obj2, **hints):
@@ -31,8 +35,8 @@ def allow_relation(self, obj1, obj2, **hints):
3135

3236
def allow_migrate(self, db, app_label, model_name=None, **hints):
3337
if app_label == "external":
34-
return db == "other"
35-
elif db == "other":
38+
return db == OTHER_DB_NAME
39+
elif db == OTHER_DB_NAME:
3640
return False
3741
else:
3842
return None
@@ -41,3 +45,34 @@ def allow_migrate(self, db, app_label, model_name=None, **hints):
4145
database_router_override_settings = {
4246
"DATABASE_ROUTERS": ["simple_history.tests.tests.utils.TestDbRouter"]
4347
}
48+
49+
50+
class TestModelWithHistoryInDifferentDbRouter(object):
51+
def db_for_read(self, model, **hints):
52+
if model == HistoricalModelWithHistoryInDifferentDb:
53+
return OTHER_DB_NAME
54+
return None
55+
56+
def db_for_write(self, model, **hints):
57+
if model == HistoricalModelWithHistoryInDifferentDb:
58+
return OTHER_DB_NAME
59+
return None
60+
61+
def allow_relation(self, obj1, obj2, **hints):
62+
if isinstance(obj1, HistoricalModelWithHistoryInDifferentDb) or isinstance(
63+
obj2, HistoricalModelWithHistoryInDifferentDb
64+
):
65+
return False
66+
return None
67+
68+
def allow_migrate(self, db, app_label, model_name=None, **hints):
69+
if model_name == HistoricalModelWithHistoryInDifferentDb._meta.model_name:
70+
return db == OTHER_DB_NAME
71+
return None
72+
73+
74+
database_router_override_settings_history_in_diff_db = {
75+
"DATABASE_ROUTERS": [
76+
"simple_history.tests.tests.utils.TestModelWithHistoryInDifferentDbRouter"
77+
]
78+
}

0 commit comments

Comments
 (0)