jazzband / django-simple-history

Store model history and view/revert changes from admin site.
https://django-simple-history.readthedocs.org
BSD 3-Clause "New" or "Revised" License
2.22k stars 480 forks source link

What's the use of HistoricForeignKey ? I can't see any foreign key relationship between automatically created related tables inside the database #1380

Open xtrm-co-in opened 3 months ago

xtrm-co-in commented 3 months ago

I was hoping that introduction of HistoricForeignKey may establish a foreignkey relationship between the historic tables of both the related tables inside the database, so that, we can easily retrieve child records while querying for the parent's history or vice versa.

For example, If I have a LineItem model having a foreign key field pointing to Invoice model, If I change either LineItem or Invoice, I will have new records for both Invoice and LineItem in their respective history tables with LineItem's historic table having history_id of Invoice's historic table so that we can easily retrieve historical data of either of them for a given point in time.

But what I found is instead of having history_id of Invoice's historical data, LineItem's historic table is actually saving original Invoice's id, which doesn't help to retrieve corresponding Invoice data in history.

Hope I made my point clear, It may have something to do with lack of documentation / example on HistoricForeignkey field. If someone could understand my point, Will you please help me achieve the same with an example preferably in context of django rest framework ?

wchen38 commented 2 months ago

Have you looked over this test case? https://github.com/jazzband/django-simple-history/blob/d8cf0b8af4726550a6060d3186a6ed895db613b9/simple_history/tests/tests/test_models.py#L2699

xtrm-co-in commented 2 months ago

Will you please clear the air? I didn't get your point.

wchen38 commented 2 months ago

from my understanding, django-simple-history library gets the historical records using timestamps, this is why the history table doesn't contain the actual history foreign keys but the original foreign keys.

I created an example to try to demonstrate what I mean. Maybe try running the test code below with your own demo DRF app. In the first test, I try to demonstrate getting historical data with explicit timestamps. In the second test, I try to use the history_date field in the history table to demonstrate that you first find out which line item record you want to look at, then you can use its history_date to fetch for the corresponding invoice at that time, this should also work vice versa. Hope it helps.

# models.py
from django.db import models
from simple_history.models import HistoricalRecords, HistoricForeignKey

class Invoice(models.Model):
    invoice_number = models.CharField(max_length=100)
    address = models.TextField(null=True, blank=True)
    date_created = models.DateField(auto_now_add=True)
    history = HistoricalRecords()

    def __str__(self):
        return self.invoice_number

class LineItem(models.Model):
    invoice = HistoricForeignKey(Invoice, related_name='line_items', on_delete=models.CASCADE)
    description = models.CharField(max_length=255)
    history = HistoricalRecords()

    def __str__(self):
        return f'{self.description} - {self.invoice.invoice_number}'
tests.py
from django.test import TestCase
from .models import LineItem, Invoice
from django.utils import timezone

# Create your tests here.

class TestHistoricalForeignKey(TestCase):
    def test_get_history_data_using_timezone_now(self):
        invoice_0 = Invoice.objects.create(invoice_number="0000", address="0000 street")
        item_0 = LineItem.objects.create(invoice=invoice_0, description="item0")
        # as of t0, the invoice and line item are created
        t0 = timezone.now()

        invoice_0.address = "0001 street"
        invoice_0.save()
        # as of t1, we updated the invoice address
        t1 = timezone.now()

        # getting the history line item as of t0
        t0_item_0 = item_0.history.as_of(t0)

        # at t0, the corresponding address should '0000 street'
        self.assertEqual(t0_item_0.invoice.address, "0000 street")

        # getting the history line item as of t1
        t1_item_0 = item_0.history.as_of(t1)
        # at t1, the corresponding address should be '0001 street'
        self.assertEqual(t1_item_0.invoice.address, "0001 street")

    def test_get_history_data_using_history_date(self):
        invoice_0 = Invoice.objects.create(invoice_number="0000", address="0000 street")
        item_0 = LineItem.objects.create(invoice=invoice_0, description="item0")

        invoice_0.address = "0001 street"
        invoice_0.save()

        # get the only record in the history table for LineItem.
        item_history_record = item_0.history.all()[0]

        # get the corresponding invoice using the line item history date. This should get the invoice record at the time when the item_history_record is created.
        invoice_record = invoice_0.history.as_of(item_history_record.history_date)

        self.assertEqual(invoice_record.address, "0000 street")