历史数据保存方案

Posted by Shallow Dreameron June 24, 2024

在使用与数据表结构一致的历史表来保存数据的方案下,如果回滚的数据中缺少新增字段的数据,而这个字段设置了非空约束,则回滚操作会遇到问题。为了解决这个问题,需要在回滚操作时为这些缺失的字段提供合理的默认值。

以下是如何处理这种情况的详细步骤:

处理步骤

  1. 在新增字段时设置临时默认值:在新增字段并设置非空约束时,可以为其设置一个临时默认值。
  2. 更新历史数据:将历史表中的旧数据更新为合适的值,确保回滚时不会出现非空约束错误。
  3. 回滚时处理缺失字段:在回滚操作中检查并填充缺失字段。

具体实现

表结构设计

CREATE TABLE main_table (
    id SERIAL PRIMARY KEY,
    order_id VARCHAR(50),
    field1 VARCHAR(255),
    field2 INT,
    new_field VARCHAR(255) NOT NULL DEFAULT 'temporary_default', -- 新增非空字段,设置临时默认值
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE history_table (
    id SERIAL PRIMARY KEY,
    main_table_id INT,
    order_id VARCHAR(50),
    field1 VARCHAR(255),
    field2 INT,
    new_field VARCHAR(255) NOT NULL DEFAULT 'temporary_default', -- 与main_table一致
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    metadata JSONB,
    FOREIGN KEY (main_table_id) REFERENCES main_table(id)
);

插入和更新操作

在每次更新主表数据时,将旧数据保存到历史表。

-- 插入旧数据到历史表
INSERT INTO history_table (main_table_id, order_id, field1, field2, new_field, created_at, updated_at, metadata)
SELECT id, order_id, field1, field2, new_field, created_at, updated_at, '{"updated_by": "user1", "update_reason": "example reason"}'::jsonb
FROM main_table
WHERE id = :id;

-- 更新主表数据
UPDATE main_table
SET field1 = :new_field1, field2 = :new_field2, new_field = :new_new_field, updated_at = CURRENT_TIMESTAMP
WHERE id = :id;

更新历史表中的旧数据

将历史表中的旧数据更新为合适的值,以确保回滚操作时不会违反非空约束。

UPDATE history_table
SET new_field = 'specified_value' -- 用实际需要的值替换临时默认值
WHERE new_field = 'temporary_default';

移除临时默认值

在确保所有数据都已更新后,移除临时默认值约束。

ALTER TABLE main_table ALTER COLUMN new_field DROP DEFAULT;
ALTER TABLE history_table ALTER COLUMN new_field DROP DEFAULT;

回滚操作

在回滚操作中检查并填充缺失字段。

-- 回滚操作示例
UPDATE main_table
SET field1 = h.field1,
    field2 = h.field2,
    new_field = COALESCE(h.new_field, 'specified_value'), -- 如果新字段值为空,则使用指定值
    updated_at = CURRENT_TIMESTAMP
FROM history_table h
WHERE main_table.id = h.main_table_id AND h.order_id = :order_id
ORDER BY h.created_at DESC
LIMIT 1;

总结

通过设置临时默认值、更新历史表中的旧数据以及在回滚操作中处理缺失字段,可以确保回滚操作成功并满足非空约束。虽然这种方法需要在新增字段时进行一些额外的操作,但它可以有效避免数据一致性问题和回滚失败,确保系统的稳定运行。


在使用JSON方案存储历史数据时,当新增字段并设置不能为空后,回滚历史数据会遇到一些挑战。具体地,当历史数据没有新添加的字段数据,而新添加的字段设置为非空时,会导致回滚失败或数据不完整。

解决方案

为了解决这个问题,可以采用以下几种方法:

  1. 设置默认值
    • 在新增字段时,设置一个合理的默认值,确保即使历史数据中没有该字段的数据,回滚时也不会违反非空约束。
  2. 数据迁移
    • 在新增字段后,立即对历史表中的旧数据进行数据迁移,给缺失新字段的历史记录填充默认值。
  3. 回滚逻辑处理
    • 在回滚操作中,检查并补充新字段的默认值,确保不违反非空约束。

实现方式

方法一:设置默认值

假设我们在 main_table 中新增一个非空字段 new_field,并设置默认值。

ALTER TABLE main_table ADD COLUMN new_field VARCHAR(255) NOT NULL DEFAULT 'default_value';

同样地,在 history_table 中也添加该字段,设置默认值:

ALTER TABLE history_table ADD COLUMN new_field VARCHAR(255) DEFAULT 'default_value';

这样,即使历史数据没有 new_field 的数据,在回滚时也会使用默认值,不会违反非空约束。

方法二:数据迁移

在新增字段后,立即对历史表中的旧数据进行迁移,给缺失新字段的历史记录填充默认值。

UPDATE history_table
SET new_field = 'default_value'
WHERE new_field IS NULL;

方法三:回滚逻辑处理

在回滚操作中,检查并补充新字段的默认值:

-- 回滚操作示例
UPDATE main_table
SET field1 = h.field1,
    field2 = h.field2,
    new_field = COALESCE(h.new_field, 'default_value'), -- 如果新字段值为空,则使用默认值
    updated_at = CURRENT_TIMESTAMP
FROM history_table h
WHERE main_table.id = h.main_table_id AND h.order_id = :order_id
ORDER BY h.created_at DESC
LIMIT 1;

示例

以下是一个完整的示例,包括主表、历史表结构定义,以及插入、更新、回滚操作:

表结构定义

CREATE TABLE main_table (
    id SERIAL PRIMARY KEY,
    order_id VARCHAR(50),
    field1 VARCHAR(255),
    field2 INT,
    new_field VARCHAR(255) NOT NULL DEFAULT 'default_value', -- 新增非空字段
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE history_table (
    id SERIAL PRIMARY KEY,
    main_table_id INT,
    order_id VARCHAR(50),
    field1 VARCHAR(255),
    field2 INT,
    new_field VARCHAR(255) DEFAULT 'default_value', -- 新增字段,设置默认值
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    metadata JSONB,
    FOREIGN KEY (main_table_id) REFERENCES main_table(id)
);

插入和更新操作

-- 插入旧数据到历史表
INSERT INTO history_table (main_table_id, order_id, field1, field2, new_field, created_at, updated_at, metadata)
SELECT id, order_id, field1, field2, new_field, created_at, updated_at, '{"updated_by": "user1", "update_reason": "example reason"}'::jsonb
FROM main_table
WHERE id = :id;

-- 更新主表数据
UPDATE main_table
SET field1 = :new_field1, field2 = :new_field2, new_field = :new_new_field, updated_at = CURRENT_TIMESTAMP
WHERE id = :id;

回滚操作

-- 回滚操作示例
UPDATE main_table
SET field1 = h.field1,
    field2 = h.field2,
    new_field = COALESCE(h.new_field, 'default_value'), -- 如果新字段值为空,则使用默认值
    updated_at = CURRENT_TIMESTAMP
FROM history_table h
WHERE main_table.id = h.main_table_id AND h.order_id = :order_id
ORDER BY h.created_at DESC
LIMIT 1;

通过设置默认值、数据迁移和回滚逻辑处理,可以确保即使在历史数据中没有新添加的字段数据,回滚操作也能成功进行,且不会违反


<style>
  main {
    width: 100%;
    padding: 20px 0 0 0;
  }
  .projection {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    align-items: center;
    padding-bottom: 20px;
    margin-bottom: 20px;
    border-bottom: 1px solid #eee;
  }
  .projection > div {
    width: 180px; height: 120px;
    background: rgba(180,160,120,.2);
  }
  .projection:not(:first-child:last-child) > div {
    border: 2px solid transparent;
    background-clip: content-box;
  }
  .projection:last-child {
    margin-bottom: 0;
    border-bottom: 0;
  }
  .projection:nth-of-type(1) > div {
    margin-left: -6px;
    box-shadow: 0 0 6px rgba(180,160,120,.8);
  }
  .projection:nth-of-type(2) > div {
    border-bottom: 0;
    box-shadow: 0px 6px 5px -5px rgba(180,160,120,.6);
  }
  .projection:nth-of-type(3) > div {
    border-right: 0;
    border-bottom: 0;
    box-shadow: 5px 5px 5px -4px rgba(180,160,120,.6);
  }
  .projection:nth-of-type(4) > div {
    border-right: 0;
    border-left: 0;
    box-shadow: 6px 0 5px -5px rgba(180,160,120,.6), -6px 0 5px -5px rgba(180,160,120,.6);
  }
  .projection:nth-of-type(5) > div {
    box-shadow: 0 0 0 1px rgba(180,160,120,.6);
  }
</style>
<template>
  <main>
    <div class="projection">
      <p>① 无偏移投影</p>
      <div></div>
    </div>
    <div class="projection">
      <p>② 单侧投影</p>
      <div></div>
    </div>
    <div class="projection">
      <p>③ 邻边投影</p>
      <div></div>
    </div>
    <div class="projection">
      <p>④ 两侧投影</p>
      <div></div>
    </div>
    <div class="projection">
      <p>⑤ 1px投影</p>
      <div></div>
    </div>
  </main>
</template>
<script>
</script>

如果历史表中的 main_table_uid 是引用主表 uid 的外键,并且你不希望使用信号来实现这个功能,可以在视图或模型方法中手动处理保存历史数据、删除旧数据和写入新数据的逻辑。

示例代码:

1. 定义模型

# models.py

from django.db import models

class MainTable(models.Model):
    uid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=255)
    data = models.TextField()

class MainTableHistory(models.Model):
    main_table_uid = models.ForeignKey(MainTable, on_delete=models.DO_NOTHING)
    name = models.CharField(max_length=255)
    data = models.TextField()
    operation_type = models.CharField(max_length=50)  # e.g., 'update', 'delete'
    changed_at = models.DateTimeField(auto_now_add=True)

2. 创建保存历史数据的方法

在你的视图或模型方法中,手动处理保存历史数据、删除旧数据和写入新数据的逻辑。

# utils.py

from .models import MainTable, MainTableHistory

def save_history(instance, operation_type):
    MainTableHistory.objects.create(
        main_table_uid=instance,
        name=instance.name,
        data=instance.data,
        operation_type=operation_type
    )

3. 更新和删除操作的视图逻辑

# views.py

from django.shortcuts import get_object_or_404, redirect
from django.http import HttpResponse
from .models import MainTable
from .utils import save_history

def update_main_table(request, uid):
    instance = get_object_or_404(MainTable, uid=uid)
    
    # 保存旧数据到历史表
    save_history(instance, 'update')

    # 更新数据
    instance.name = request.POST.get('name')
    instance.data = request.POST.get('data')
    instance.save()

    return HttpResponse("Data updated successfully.")

def delete_main_table(request, uid):
    instance = get_object_or_404(MainTable, uid=uid)
    
    # 保存数据到历史表
    save_history(instance, 'delete')

    # 删除数据
    instance.delete()

    return HttpResponse("Data deleted successfully.")

4. URL 配置

# urls.py

from django.urls import path
from .views import update_main_table, delete_main_table

urlpatterns = [
    path('update/<int:uid>/', update_main_table, name='update_main_table'),
    path('delete/<int:uid>/', delete_main_table, name='delete_main_table'),
]

解释

  1. 模型定义
    • MainTable 是主表。
    • MainTableHistory 是历史数据表,用于保存删除或更新的主表数据。它引用 MainTableuid,并增加了 operation_type 字段来区分操作类型(更新或删除)。
  2. 保存历史数据的方法
    • save_history 方法用于将旧数据保存到历史数据表中。它接收一个 MainTable 实例和操作类型(’update’ 或 ‘delete’),并将相关数据保存到 MainTableHistory 表中。
  3. 更新和删除操作的视图逻辑
    • update_main_table 视图函数用于处理更新操作。它首先调用 save_history 方法保存旧数据,然后更新 MainTable 实例并保存。
    • delete_main_table 视图函数用于处理删除操作。它首先调用 save_history 方法保存数据,然后删除 MainTable 实例。

通过这种方式,你可以在视图中显式地处理保存历史数据、删除旧数据和写入新数据的逻辑。这可以避免使用信号,同时确保历史数据的完整性。


在这种情况下,你的更新操作包含以下几个步骤:

  1. 保存历史数据:在更新之前,将旧数据保存到历史数据表中。
  2. 删除原数据:从主表中删除旧数据。
  3. 写入新数据:将新数据写入主表。

你可以通过 Django 的信号机制来实现这一过程。具体来说,可以使用 pre_save 信号在更新操作之前保存旧数据到历史表,然后继续删除旧数据并保存新数据。

示例代码:

1. 定义模型

# models.py

from django.db import models

class MainTable(models.Model):
    uid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=255)
    data = models.TextField()

class MainTableHistory(models.Model):
    main_table_uid = models.ForeignKey(MainTable, on_delete=models.DO_NOTHING)
    name = models.CharField(max_length=255)
    data = models.TextField()
    operation_type = models.CharField(max_length=50)  # e.g., 'update', 'delete'
    changed_at = models.DateTimeField(auto_now_add=True)

2. 创建信号处理函数

# signals.py

from django.db.models.signals import pre_save, pre_delete
from django.dispatch import receiver
from .models import MainTable, MainTableHistory
from django.core.exceptions import ObjectDoesNotExist

@receiver(pre_save, sender=MainTable)
def save_update_to_history(sender, instance, **kwargs):
    if instance.pk:
        try:
            old_instance = MainTable.objects.get(pk=instance.pk)
            if old_instance.name != instance.name or old_instance.data != instance.data:
                MainTableHistory.objects.create(
                    main_table_uid=old_instance,
                    name=old_instance.name,
                    data=old_instance.data,
                    operation_type='update'
                )
        except ObjectDoesNotExist:
            print(f"Original instance not found for pk {instance.pk}")
        except Exception as e:
            print(f"Error saving update history: {e}")

@receiver(pre_delete, sender=MainTable)
def save_delete_to_history(sender, instance, **kwargs):
    try:
        if not MainTableHistory.objects.filter(main_table_uid=instance, operation_type='delete').exists():
            MainTableHistory.objects.create(
                main_table_uid=instance,
                name=instance.name,
                data=instance.data,
                operation_type='delete'
            )
    except Exception as e:
        print(f"Error saving delete history: {e}")

3. 连接信号

确保 Django 在启动时加载信号处理函数。可以在应用程序的 apps.py 文件中进行配置。

# apps.py

from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        import myapp.signals  # 确保信号处理函数被导入

4. 设置应用配置

确保在 settings.py 中设置正确的应用配置。

# settings.py

INSTALLED_APPS = [
    ...
    'myapp.apps.MyAppConfig',
    ...
]

解释

  1. 模型定义
    • MainTable 是主表。
    • MainTableHistory 是历史数据表,用于保存删除或更新的主表数据。它引用 MainTableuid,并增加了 operation_type 字段来区分操作类型(更新或删除)。
  2. 信号处理函数
    • save_update_to_history 函数在更新主表数据之前,将旧的数据保存到历史数据表中。
    • save_delete_to_history 函数在删除主表数据之前,将数据保存到历史数据表中,并确保不会有重复的记录。
  3. 连接信号
    • apps.py 中的 ready 方法中导入 signals 模块,确保 Django 启动时加载信号处理函数。

通过这种方式,你可以确保在更新和删除主表数据时,相关的数据会先保存到历史数据表中。这样可以避免在更新和删除操作时出现问题,并确保历史数据的完整性。