Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the use of multiple databases #539

Merged
merged 14 commits into from
Apr 15, 2019
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Authors
- Nianpeng Li
- Nick Träger
- Phillip Marshall
- Prakash Venkatraman (`dopatraman <https://github.com/dopatraman>`_)
- Rajesh Pappula
- Ray Logel
- Roberto Aguilar
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Unreleased
----------
- Added the possibility to create a relation to the original model

2.7.1 (2019-03-26)
------------------
- Added routing for saving historical records to separate databases if necessary.

2.7.0 (2019-01-16)
------------------
- Add support for ``using`` chained manager method and save/delete keyword argument (gh-507)
Expand Down
15 changes: 15 additions & 0 deletions docs/multiple_dbs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,18 @@ an issue where you want to track the history on a table that lives in a separate
database to your user model. Since Django does not support cross-database relations,
you will have to manually track the ``history_user`` using an explicit ID. The full
documentation on this feature is in :ref:`Manually Track User Model`.

Tracking History Separate from the Base Model
---------------------------------------------
You can choose whether or not to track models' history in the same database by
setting the flag `use_base_model_db`.

```
class MyModel(models.Model):
...
history = HistoricalRecords(use_base_model_db=False)
```

If set to `True`, migrations and audit
events will be sent to the same database as the base model. If `False`, they
will be sent to the place specified by the database router.
12 changes: 10 additions & 2 deletions simple_history/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def __init__(
history_user_getter=_history_user_getter,
history_user_setter=_history_user_setter,
related_name=None,
use_base_model_db=True,
):
self.user_set_verbose_name = verbose_name
self.user_related_name = user_related_name
Expand All @@ -95,6 +96,7 @@ def __init__(
self.user_getter = history_user_getter
self.user_setter = history_user_setter
self.related_name = related_name
self.use_base_model_db = use_base_model_db

if excluded_fields is None:
excluded_fields = []
Expand Down Expand Up @@ -433,14 +435,20 @@ def post_save(self, instance, created, using=None, **kwargs):
if not created and hasattr(instance, "skip_history_when_saving"):
return
if not kwargs.get("raw", False):
self.create_historical_record(instance, created and "+" or "~", using=using)
self.create_historical_record(
instance,
created and "+" or "~",
using=using if self.use_base_model_db else None,
)

def post_delete(self, instance, using=None, **kwargs):
if self.cascade_delete_history:
manager = getattr(instance, self.manager_name)
manager.using(using).all().delete()
else:
self.create_historical_record(instance, "-", using=using)
self.create_historical_record(
instance, "-", using=using if self.use_base_model_db else None
)

def create_historical_record(self, instance, history_type, using=None):
history_date = getattr(instance, "_history_date", now())
Expand Down
5 changes: 5 additions & 0 deletions simple_history/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ class ModelWithHistoryInDifferentApp(models.Model):
history = HistoricalRecords(app="external")


class ModelWithHistoryInDifferentDb(models.Model):
name = models.CharField(max_length=30)
history = HistoricalRecords(use_base_model_db=False)


###############################################################################
#
# Inheritance examples
Expand Down
50 changes: 49 additions & 1 deletion simple_history/tests/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@
from simple_history import register
from simple_history.exceptions import RelatedNameConflictError
from simple_history.models import HistoricalRecords, ModelChange
from simple_history.signals import pre_create_historical_record
from simple_history.signals import (
pre_create_historical_record,
post_create_historical_record,
)
from simple_history.tests.custom_user.models import CustomUser
from simple_history.tests.tests.utils import (
database_router_override_settings,
middleware_override_settings,
database_router_override_settings_history_in_diff_db,
)
from simple_history.utils import get_history_model_for_model
from simple_history.utils import update_change_reason
Expand Down Expand Up @@ -62,6 +66,7 @@
HistoricalPollWithHistoricalIPAddress,
HistoricalState,
Library,
ModelWithHistoryInDifferentDb,
MultiOneToOne,
Person,
Place,
Expand Down Expand Up @@ -1478,3 +1483,46 @@ def test_revert(self):

self.one = Street.objects.get(pk=id)
self.assertEqual(self.one.history.count(), 4)


@override_settings(**database_router_override_settings_history_in_diff_db)
class SaveHistoryInSeparateDatabaseTestCase(TestCase):
multi_db = True

def setUp(self):
self.model = ModelWithHistoryInDifferentDb.objects.create(name="test")

def test_history_model_saved_in_separate_db(self):
self.assertEqual(0, self.model.history.using("default").count())
dopatraman marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(1, self.model.history.count())
self.assertEqual(1, self.model.history.using("other").count())
self.assertEqual(
1, ModelWithHistoryInDifferentDb.objects.using("default").count()
)
self.assertEqual(1, ModelWithHistoryInDifferentDb.objects.count())
self.assertEqual(
0, ModelWithHistoryInDifferentDb.objects.using("other").count()
dopatraman marked this conversation as resolved.
Show resolved Hide resolved
)

def test_history_model_saved_in_separate_db_on_delete(self):
id = self.model.id
self.model.delete()

self.assertEqual(
0,
ModelWithHistoryInDifferentDb.history.using("default")
.filter(id=id)
.count(),
)
self.assertEqual(2, ModelWithHistoryInDifferentDb.history.filter(id=id).count())
self.assertEqual(
2,
ModelWithHistoryInDifferentDb.history.using("other").filter(id=id).count(),
)
self.assertEqual(
0, ModelWithHistoryInDifferentDb.objects.using("default").count()
)
self.assertEqual(0, ModelWithHistoryInDifferentDb.objects.count())
self.assertEqual(
0, ModelWithHistoryInDifferentDb.objects.using("other").count()
)
43 changes: 39 additions & 4 deletions simple_history/tests/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import django
from django.conf import settings

from simple_history.tests.models import HistoricalModelWithHistoryInDifferentDb

request_middleware = "simple_history.middleware.HistoryRequestMiddleware"

OTHER_DB_NAME = "other"

if django.__version__ >= "2.0":
middleware_override_settings = {
"MIDDLEWARE": (settings.MIDDLEWARE + [request_middleware])
Expand All @@ -16,12 +20,12 @@
class TestDbRouter(object):
def db_for_read(self, model, **hints):
if model._meta.app_label == "external":
return "other"
return OTHER_DB_NAME
return None

def db_for_write(self, model, **hints):
if model._meta.app_label == "external":
return "other"
return OTHER_DB_NAME
return None

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

def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label == "external":
return db == "other"
elif db == "other":
return db == OTHER_DB_NAME
elif db == OTHER_DB_NAME:
return False
else:
return None
Expand All @@ -41,3 +45,34 @@ def allow_migrate(self, db, app_label, model_name=None, **hints):
database_router_override_settings = {
"DATABASE_ROUTERS": ["simple_history.tests.tests.utils.TestDbRouter"]
}


class TestModelWithHistoryInDifferentDbRouter(object):
def db_for_read(self, model, **hints):
if model == HistoricalModelWithHistoryInDifferentDb:
return OTHER_DB_NAME
return None

def db_for_write(self, model, **hints):
if model == HistoricalModelWithHistoryInDifferentDb:
return OTHER_DB_NAME
return None

def allow_relation(self, obj1, obj2, **hints):
if isinstance(obj1, HistoricalModelWithHistoryInDifferentDb) or isinstance(
obj2, HistoricalModelWithHistoryInDifferentDb
):
return False
return None

def allow_migrate(self, db, app_label, model_name=None, **hints):
if model_name == HistoricalModelWithHistoryInDifferentDb._meta.model_name:
return db == OTHER_DB_NAME
return None


database_router_override_settings_history_in_diff_db = {
"DATABASE_ROUTERS": [
"simple_history.tests.tests.utils.TestModelWithHistoryInDifferentDbRouter"
]
}