diff --git a/core/models/history_model.py b/core/models/history_model.py index c008124..959d75b 100644 --- a/core/models/history_model.py +++ b/core/models/history_model.py @@ -6,7 +6,7 @@ import datetime as base_datetime from dirtyfields import DirtyFieldsMixin from django.core.exceptions import ValidationError -from django.db import models +from django.db import models, transaction from django.db.models import F from simple_history.models import HistoricalRecords from core.utils import CachedManager, CachedModelMixin @@ -217,6 +217,84 @@ def copy(self, exclude_fields=["id", "uuid"]): return new_instance + @classmethod + def bulk_save(cls, data_list, user, batch_size=100): + """ + Efficiently update or create multiple instances based on 'id' field. + All operations are atomic - either all succeed or all fail. + + Args: + data_list: List of dicts with instance data (with or without 'id') + user: User performing the operation + batch_size: Number of records to process per batch + + Returns: + dict with 'created' and 'updated' counts + """ + if not data_list: + return {'created': 0, 'updated': 0} + + now = py_datetime.now() + + ids_to_update = [d['id'] for d in data_list if d.get('id')] + + existing = {obj.id: obj for obj in cls.objects.filter(id__in=ids_to_update, is_deleted=False)} + + to_create = [] + to_update = [] + + exclude_fields = {'id', 'uuid', 'date_created', 'user_created', 'date_updated', + 'user_updated', 'version', 'is_deleted', 'date_valid_from', + 'date_valid_to', 'replacement_uuid'} + + for data in data_list: + record_id = data.get('id') + + if record_id and record_id in existing: + instance = existing[record_id] + for field, value in data.items(): + if field not in exclude_fields: + setattr(instance, field, value) + instance.user_updated = user + instance.date_updated = now + instance.version = F('version') + 1 + to_update.append(instance) + else: + create_data = {k: v for k, v in data.items() if k not in exclude_fields} + instance = cls(**create_data) + instance.set_pk() + instance.user_created = user + instance.user_updated = user + instance.date_created = now + instance.date_updated = now + instance.version = 1 + to_create.append(instance) + + with transaction.atomic(): + created_count = 0 + updated_count = 0 + + if to_create: + cls.objects.bulk_create(to_create, batch_size=batch_size) + created_count = len(to_create) + + if to_update: + update_fields = [f for f in to_update[0].__dict__.keys() + if not f.startswith('_') and f not in exclude_fields] + update_fields += ['user_updated', 'date_updated', 'version'] + + cls.objects.bulk_update(to_update, update_fields, batch_size=batch_size) + + ids = [obj.id for obj in to_update] + updated_objects = cls.objects.filter(id__in=ids).only('id', 'version') + version_map = {obj.id: obj.version for obj in updated_objects} + for obj in to_update: + obj.version = version_map.get(obj.id, obj.version) + + updated_count = len(to_update) + + return {'created': created_count, 'updated': updated_count} + @classmethod def filter_queryset(cls, queryset=None): if queryset is None: