django-bulk-hooks 0.1.203__py3-none-any.whl → 0.1.205__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bulk-hooks might be problematic. Click here for more details.
- django_bulk_hooks/decorators.py +43 -52
- django_bulk_hooks/engine.py +4 -76
- django_bulk_hooks/handler.py +12 -53
- django_bulk_hooks/queryset.py +714 -803
- {django_bulk_hooks-0.1.203.dist-info → django_bulk_hooks-0.1.205.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.1.203.dist-info → django_bulk_hooks-0.1.205.dist-info}/RECORD +8 -8
- {django_bulk_hooks-0.1.203.dist-info → django_bulk_hooks-0.1.205.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.203.dist-info → django_bulk_hooks-0.1.205.dist-info}/WHEEL +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,803 +1,714 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
from django.db.models import AutoField
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
""
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
# If objects have pk set but are not loaded from DB, use those PKs
|
|
716
|
-
root_pks = []
|
|
717
|
-
with self._profile_step("mti_bulk_update.collect_root_pks", root_model, extra=f"batch_n={len(batch)}"):
|
|
718
|
-
for obj in batch:
|
|
719
|
-
# Check both pk and id attributes
|
|
720
|
-
pk_value = getattr(obj, 'pk', None)
|
|
721
|
-
if pk_value is None:
|
|
722
|
-
pk_value = getattr(obj, 'id', None)
|
|
723
|
-
|
|
724
|
-
if pk_value is not None:
|
|
725
|
-
root_pks.append(pk_value)
|
|
726
|
-
else:
|
|
727
|
-
continue
|
|
728
|
-
|
|
729
|
-
if not root_pks:
|
|
730
|
-
return 0
|
|
731
|
-
|
|
732
|
-
# Update each table in the inheritance chain
|
|
733
|
-
for model, model_fields in field_groups.items():
|
|
734
|
-
if not model_fields:
|
|
735
|
-
continue
|
|
736
|
-
|
|
737
|
-
if model == inheritance_chain[0]:
|
|
738
|
-
# Root model - use primary keys directly
|
|
739
|
-
pks = root_pks
|
|
740
|
-
filter_field = 'pk'
|
|
741
|
-
else:
|
|
742
|
-
# Child model - use parent link field
|
|
743
|
-
parent_link = None
|
|
744
|
-
for parent_model in inheritance_chain:
|
|
745
|
-
if parent_model in model._meta.parents:
|
|
746
|
-
parent_link = model._meta.parents[parent_model]
|
|
747
|
-
break
|
|
748
|
-
|
|
749
|
-
if parent_link is None:
|
|
750
|
-
continue
|
|
751
|
-
|
|
752
|
-
# For child models, the parent link values should be the same as root PKs
|
|
753
|
-
pks = root_pks
|
|
754
|
-
filter_field = parent_link.attname
|
|
755
|
-
|
|
756
|
-
if pks:
|
|
757
|
-
base_qs = model._base_manager.using(self.db)
|
|
758
|
-
|
|
759
|
-
# Check if records exist
|
|
760
|
-
with self._profile_step("mti_bulk_update.exists_check", model, extra=f"n={len(pks)}"):
|
|
761
|
-
existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
|
|
762
|
-
|
|
763
|
-
if existing_count == 0:
|
|
764
|
-
continue
|
|
765
|
-
|
|
766
|
-
# Build CASE statements for each field to perform a single bulk update
|
|
767
|
-
with self._profile_step("mti_bulk_update.build_case", model, extra=f"fields={len(model_fields)}"):
|
|
768
|
-
case_statements = {}
|
|
769
|
-
for field_name in model_fields:
|
|
770
|
-
field = model._meta.get_field(field_name)
|
|
771
|
-
when_statements = []
|
|
772
|
-
|
|
773
|
-
for pk, obj in zip(pks, batch):
|
|
774
|
-
# Check both pk and id attributes for the object
|
|
775
|
-
obj_pk = getattr(obj, 'pk', None)
|
|
776
|
-
if obj_pk is None:
|
|
777
|
-
obj_pk = getattr(obj, 'id', None)
|
|
778
|
-
|
|
779
|
-
if obj_pk is None:
|
|
780
|
-
continue
|
|
781
|
-
value = getattr(obj, field_name)
|
|
782
|
-
when_statements.append(When(**{filter_field: pk}, then=Value(value, output_field=field)))
|
|
783
|
-
|
|
784
|
-
case_statements[field_name] = Case(*when_statements, output_field=field)
|
|
785
|
-
|
|
786
|
-
# Execute a single bulk update for all objects in this model
|
|
787
|
-
try:
|
|
788
|
-
with self._profile_step("mti_bulk_update.update", model, extra=f"n={len(pks)} fields={len(model_fields)}"):
|
|
789
|
-
updated_count = base_qs.filter(**{f"{filter_field}__in": pks}).update(**case_statements)
|
|
790
|
-
total_updated += updated_count
|
|
791
|
-
except Exception as e:
|
|
792
|
-
import traceback
|
|
793
|
-
traceback.print_exc()
|
|
794
|
-
|
|
795
|
-
return total_updated
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
class HookQuerySet(HookQuerySetMixin, models.QuerySet):
|
|
799
|
-
"""
|
|
800
|
-
A QuerySet that provides bulk hook functionality.
|
|
801
|
-
This is the traditional approach for backward compatibility.
|
|
802
|
-
"""
|
|
803
|
-
pass
|
|
1
|
+
from django.db import models, transaction
|
|
2
|
+
from django.db.models import AutoField
|
|
3
|
+
|
|
4
|
+
from django_bulk_hooks import engine
|
|
5
|
+
from django_bulk_hooks.constants import (
|
|
6
|
+
AFTER_CREATE,
|
|
7
|
+
AFTER_DELETE,
|
|
8
|
+
AFTER_UPDATE,
|
|
9
|
+
BEFORE_CREATE,
|
|
10
|
+
BEFORE_DELETE,
|
|
11
|
+
BEFORE_UPDATE,
|
|
12
|
+
VALIDATE_CREATE,
|
|
13
|
+
VALIDATE_DELETE,
|
|
14
|
+
VALIDATE_UPDATE,
|
|
15
|
+
)
|
|
16
|
+
from django_bulk_hooks.context import HookContext
|
|
17
|
+
from django.db.models import When, Value, Case, Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HookQuerySetMixin:
|
|
21
|
+
"""
|
|
22
|
+
A mixin that provides bulk hook functionality to any QuerySet.
|
|
23
|
+
This can be dynamically injected into querysets from other managers.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@transaction.atomic
|
|
27
|
+
def delete(self):
|
|
28
|
+
objs = list(self)
|
|
29
|
+
if not objs:
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
model_cls = self.model
|
|
33
|
+
ctx = HookContext(model_cls)
|
|
34
|
+
|
|
35
|
+
# Run validation hooks first
|
|
36
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
37
|
+
|
|
38
|
+
# Then run business logic hooks
|
|
39
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
40
|
+
|
|
41
|
+
# Use Django's standard delete() method
|
|
42
|
+
result = super().delete()
|
|
43
|
+
|
|
44
|
+
# Run AFTER_DELETE hooks
|
|
45
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
@transaction.atomic
|
|
50
|
+
def update(self, **kwargs):
|
|
51
|
+
instances = list(self)
|
|
52
|
+
if not instances:
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
model_cls = self.model
|
|
56
|
+
pks = [obj.pk for obj in instances]
|
|
57
|
+
|
|
58
|
+
# Load originals for hook comparison and ensure they match the order of instances
|
|
59
|
+
# Use the base manager to avoid recursion
|
|
60
|
+
original_map = {
|
|
61
|
+
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
62
|
+
}
|
|
63
|
+
originals = [original_map.get(obj.pk) for obj in instances]
|
|
64
|
+
|
|
65
|
+
# Apply field updates to instances
|
|
66
|
+
for obj in instances:
|
|
67
|
+
for field, value in kwargs.items():
|
|
68
|
+
setattr(obj, field, value)
|
|
69
|
+
|
|
70
|
+
# Run BEFORE_UPDATE hooks
|
|
71
|
+
ctx = HookContext(model_cls)
|
|
72
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
73
|
+
|
|
74
|
+
# Use Django's built-in update logic directly
|
|
75
|
+
# Call the base QuerySet implementation to avoid recursion
|
|
76
|
+
update_count = super().update(**kwargs)
|
|
77
|
+
|
|
78
|
+
# Run AFTER_UPDATE hooks
|
|
79
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
80
|
+
|
|
81
|
+
return update_count
|
|
82
|
+
|
|
83
|
+
@transaction.atomic
|
|
84
|
+
def bulk_create(
|
|
85
|
+
self,
|
|
86
|
+
objs,
|
|
87
|
+
batch_size=None,
|
|
88
|
+
ignore_conflicts=False,
|
|
89
|
+
update_conflicts=False,
|
|
90
|
+
update_fields=None,
|
|
91
|
+
unique_fields=None,
|
|
92
|
+
bypass_hooks=False,
|
|
93
|
+
bypass_validation=False,
|
|
94
|
+
):
|
|
95
|
+
"""
|
|
96
|
+
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
97
|
+
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
98
|
+
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
99
|
+
"""
|
|
100
|
+
model_cls = self.model
|
|
101
|
+
|
|
102
|
+
# When you bulk insert you don't get the primary keys back (if it's an
|
|
103
|
+
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
104
|
+
# you can't insert into the child tables which references this. There
|
|
105
|
+
# are two workarounds:
|
|
106
|
+
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
107
|
+
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
108
|
+
# tables to get the primary keys back and then doing a single bulk
|
|
109
|
+
# insert into the childmost table.
|
|
110
|
+
# We currently set the primary keys on the objects when using
|
|
111
|
+
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
112
|
+
# Oracle as well, but the semantics for extracting the primary keys is
|
|
113
|
+
# trickier so it's not done yet.
|
|
114
|
+
if batch_size is not None and batch_size <= 0:
|
|
115
|
+
raise ValueError("Batch size must be a positive integer.")
|
|
116
|
+
|
|
117
|
+
if not objs:
|
|
118
|
+
return objs
|
|
119
|
+
|
|
120
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
121
|
+
raise TypeError(
|
|
122
|
+
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
126
|
+
# This follows Django's approach: check that the parents share the same concrete model
|
|
127
|
+
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
128
|
+
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
129
|
+
# identify that case as involving multiple tables.
|
|
130
|
+
is_mti = False
|
|
131
|
+
for parent in model_cls._meta.all_parents:
|
|
132
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
133
|
+
is_mti = True
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
# Fire hooks before DB ops
|
|
137
|
+
if not bypass_hooks:
|
|
138
|
+
ctx = HookContext(model_cls)
|
|
139
|
+
if not bypass_validation:
|
|
140
|
+
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
141
|
+
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
142
|
+
|
|
143
|
+
# For MTI models, we need to handle them specially
|
|
144
|
+
if is_mti:
|
|
145
|
+
# Use our MTI-specific logic
|
|
146
|
+
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
147
|
+
mti_kwargs = {
|
|
148
|
+
"batch_size": batch_size,
|
|
149
|
+
"ignore_conflicts": ignore_conflicts,
|
|
150
|
+
"update_conflicts": update_conflicts,
|
|
151
|
+
"update_fields": update_fields,
|
|
152
|
+
"unique_fields": unique_fields,
|
|
153
|
+
}
|
|
154
|
+
# Remove custom hook kwargs if present in self.bulk_create signature
|
|
155
|
+
result = self._mti_bulk_create(
|
|
156
|
+
objs,
|
|
157
|
+
**mti_kwargs,
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
# For single-table models, use Django's built-in bulk_create
|
|
161
|
+
# but we need to call it on the base manager to avoid recursion
|
|
162
|
+
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
163
|
+
|
|
164
|
+
result = super().bulk_create(
|
|
165
|
+
objs,
|
|
166
|
+
batch_size=batch_size,
|
|
167
|
+
ignore_conflicts=ignore_conflicts,
|
|
168
|
+
update_conflicts=update_conflicts,
|
|
169
|
+
update_fields=update_fields,
|
|
170
|
+
unique_fields=unique_fields,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Fire AFTER_CREATE hooks
|
|
174
|
+
if not bypass_hooks:
|
|
175
|
+
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
176
|
+
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
@transaction.atomic
|
|
180
|
+
def bulk_update(
|
|
181
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
Bulk update objects in the database with MTI support.
|
|
185
|
+
"""
|
|
186
|
+
model_cls = self.model
|
|
187
|
+
|
|
188
|
+
if not objs:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
192
|
+
raise TypeError(
|
|
193
|
+
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Check for MTI
|
|
197
|
+
is_mti = False
|
|
198
|
+
for parent in model_cls._meta.all_parents:
|
|
199
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
200
|
+
is_mti = True
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if not bypass_hooks:
|
|
204
|
+
# Load originals for hook comparison
|
|
205
|
+
original_map = {
|
|
206
|
+
obj.pk: obj
|
|
207
|
+
for obj in model_cls._base_manager.filter(
|
|
208
|
+
pk__in=[obj.pk for obj in objs]
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
originals = [original_map.get(obj.pk) for obj in objs]
|
|
212
|
+
|
|
213
|
+
ctx = HookContext(model_cls)
|
|
214
|
+
|
|
215
|
+
# Run validation hooks first
|
|
216
|
+
if not bypass_validation:
|
|
217
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
218
|
+
|
|
219
|
+
# Then run business logic hooks
|
|
220
|
+
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
221
|
+
|
|
222
|
+
# Detect modified fields during hooks
|
|
223
|
+
modified_fields = self._detect_modified_fields(objs, originals)
|
|
224
|
+
if modified_fields:
|
|
225
|
+
fields_set = set(fields)
|
|
226
|
+
fields_set.update(modified_fields)
|
|
227
|
+
fields = list(fields_set)
|
|
228
|
+
|
|
229
|
+
# Handle auto_now fields like Django's update_or_create does
|
|
230
|
+
fields_set = set(fields)
|
|
231
|
+
pk_fields = model_cls._meta.pk_fields
|
|
232
|
+
for field in model_cls._meta.local_concrete_fields:
|
|
233
|
+
# Only add auto_now fields (like updated_at) that aren't already in the fields list
|
|
234
|
+
# Don't include auto_now_add fields (like created_at) as they should only be set on creation
|
|
235
|
+
if hasattr(field, "auto_now") and field.auto_now:
|
|
236
|
+
if field.name not in fields_set and field.name not in pk_fields:
|
|
237
|
+
fields_set.add(field.name)
|
|
238
|
+
if field.name != field.attname:
|
|
239
|
+
fields_set.add(field.attname)
|
|
240
|
+
fields = list(fields_set)
|
|
241
|
+
|
|
242
|
+
# Handle MTI models differently
|
|
243
|
+
if is_mti:
|
|
244
|
+
result = self._mti_bulk_update(objs, fields, **kwargs)
|
|
245
|
+
else:
|
|
246
|
+
# For single-table models, use Django's built-in bulk_update
|
|
247
|
+
django_kwargs = {
|
|
248
|
+
k: v
|
|
249
|
+
for k, v in kwargs.items()
|
|
250
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
251
|
+
}
|
|
252
|
+
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
253
|
+
|
|
254
|
+
if not bypass_hooks:
|
|
255
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
256
|
+
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
def _detect_modified_fields(self, new_instances, original_instances):
|
|
260
|
+
"""
|
|
261
|
+
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
262
|
+
new instances with their original values.
|
|
263
|
+
"""
|
|
264
|
+
if not original_instances:
|
|
265
|
+
return set()
|
|
266
|
+
|
|
267
|
+
modified_fields = set()
|
|
268
|
+
|
|
269
|
+
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
270
|
+
for new_instance, original in zip(new_instances, original_instances):
|
|
271
|
+
if new_instance.pk is None or original is None:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Compare all fields to detect changes
|
|
275
|
+
for field in new_instance._meta.fields:
|
|
276
|
+
if field.name == "id":
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
new_value = getattr(new_instance, field.name)
|
|
280
|
+
original_value = getattr(original, field.name)
|
|
281
|
+
|
|
282
|
+
# Handle different field types appropriately
|
|
283
|
+
if field.is_relation:
|
|
284
|
+
# For foreign keys, compare the pk values
|
|
285
|
+
new_pk = new_value.pk if new_value else None
|
|
286
|
+
original_pk = original_value.pk if original_value else None
|
|
287
|
+
if new_pk != original_pk:
|
|
288
|
+
modified_fields.add(field.name)
|
|
289
|
+
else:
|
|
290
|
+
# For regular fields, use direct comparison
|
|
291
|
+
if new_value != original_value:
|
|
292
|
+
modified_fields.add(field.name)
|
|
293
|
+
|
|
294
|
+
return modified_fields
|
|
295
|
+
|
|
296
|
+
def _get_inheritance_chain(self):
|
|
297
|
+
"""
|
|
298
|
+
Get the complete inheritance chain from root parent to current model.
|
|
299
|
+
Returns list of model classes in order: [RootParent, Parent, Child]
|
|
300
|
+
"""
|
|
301
|
+
chain = []
|
|
302
|
+
current_model = self.model
|
|
303
|
+
while current_model:
|
|
304
|
+
if not current_model._meta.proxy:
|
|
305
|
+
chain.append(current_model)
|
|
306
|
+
|
|
307
|
+
parents = [
|
|
308
|
+
parent
|
|
309
|
+
for parent in current_model._meta.parents.keys()
|
|
310
|
+
if not parent._meta.proxy
|
|
311
|
+
]
|
|
312
|
+
current_model = parents[0] if parents else None
|
|
313
|
+
|
|
314
|
+
chain.reverse()
|
|
315
|
+
return chain
|
|
316
|
+
|
|
317
|
+
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
318
|
+
"""
|
|
319
|
+
Implements Django's suggested workaround #2 for MTI bulk_create:
|
|
320
|
+
O(n) normal inserts into parent tables to get primary keys back,
|
|
321
|
+
then single bulk insert into childmost table.
|
|
322
|
+
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
323
|
+
"""
|
|
324
|
+
# Remove custom hook kwargs before passing to Django internals
|
|
325
|
+
django_kwargs = {
|
|
326
|
+
k: v
|
|
327
|
+
for k, v in kwargs.items()
|
|
328
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
329
|
+
}
|
|
330
|
+
if inheritance_chain is None:
|
|
331
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
332
|
+
|
|
333
|
+
# Safety check to prevent infinite recursion
|
|
334
|
+
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
335
|
+
raise ValueError(
|
|
336
|
+
"Inheritance chain too deep - possible infinite recursion detected"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
340
|
+
created_objects = []
|
|
341
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
342
|
+
for i in range(0, len(objs), batch_size):
|
|
343
|
+
batch = objs[i : i + batch_size]
|
|
344
|
+
batch_result = self._process_mti_bulk_create_batch(
|
|
345
|
+
batch, inheritance_chain, **django_kwargs
|
|
346
|
+
)
|
|
347
|
+
created_objects.extend(batch_result)
|
|
348
|
+
return created_objects
|
|
349
|
+
|
|
350
|
+
def _process_mti_bulk_create_batch(self, batch, inheritance_chain, **kwargs):
|
|
351
|
+
"""
|
|
352
|
+
Process a single batch of objects through the inheritance chain.
|
|
353
|
+
Implements Django's suggested workaround #2: O(n) normal inserts into parent
|
|
354
|
+
tables to get primary keys back, then single bulk insert into childmost table.
|
|
355
|
+
"""
|
|
356
|
+
# For MTI, we need to save parent objects first to get PKs
|
|
357
|
+
# Then we can use Django's bulk_create for the child objects
|
|
358
|
+
parent_objects_map = {}
|
|
359
|
+
|
|
360
|
+
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
361
|
+
# Get bypass_hooks from kwargs
|
|
362
|
+
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
363
|
+
bypass_validation = kwargs.get("bypass_validation", False)
|
|
364
|
+
|
|
365
|
+
for obj in batch:
|
|
366
|
+
parent_instances = {}
|
|
367
|
+
current_parent = None
|
|
368
|
+
for model_class in inheritance_chain[:-1]:
|
|
369
|
+
parent_obj = self._create_parent_instance(
|
|
370
|
+
obj, model_class, current_parent
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Fire parent hooks if not bypassed
|
|
374
|
+
if not bypass_hooks:
|
|
375
|
+
ctx = HookContext(model_class)
|
|
376
|
+
if not bypass_validation:
|
|
377
|
+
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
378
|
+
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
379
|
+
|
|
380
|
+
# Use Django's base manager to create the object and get PKs back
|
|
381
|
+
# This bypasses hooks and the MTI exception
|
|
382
|
+
field_values = {
|
|
383
|
+
field.name: getattr(parent_obj, field.name)
|
|
384
|
+
for field in model_class._meta.local_fields
|
|
385
|
+
if hasattr(parent_obj, field.name)
|
|
386
|
+
and getattr(parent_obj, field.name) is not None
|
|
387
|
+
}
|
|
388
|
+
created_obj = model_class._base_manager.using(self.db).create(
|
|
389
|
+
**field_values
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Update the parent_obj with the created object's PK
|
|
393
|
+
parent_obj.pk = created_obj.pk
|
|
394
|
+
parent_obj._state.adding = False
|
|
395
|
+
parent_obj._state.db = self.db
|
|
396
|
+
|
|
397
|
+
# Fire AFTER_CREATE hooks for parent
|
|
398
|
+
if not bypass_hooks:
|
|
399
|
+
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
400
|
+
|
|
401
|
+
parent_instances[model_class] = parent_obj
|
|
402
|
+
current_parent = parent_obj
|
|
403
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
404
|
+
|
|
405
|
+
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
406
|
+
child_model = inheritance_chain[-1]
|
|
407
|
+
all_child_objects = []
|
|
408
|
+
for obj in batch:
|
|
409
|
+
child_obj = self._create_child_instance(
|
|
410
|
+
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
411
|
+
)
|
|
412
|
+
all_child_objects.append(child_obj)
|
|
413
|
+
|
|
414
|
+
# Step 2.5: Use Django's internal bulk_create infrastructure
|
|
415
|
+
if all_child_objects:
|
|
416
|
+
# Get the base manager's queryset
|
|
417
|
+
base_qs = child_model._base_manager.using(self.db)
|
|
418
|
+
|
|
419
|
+
# Use Django's exact approach: call _prepare_for_bulk_create then partition
|
|
420
|
+
base_qs._prepare_for_bulk_create(all_child_objects)
|
|
421
|
+
|
|
422
|
+
# Implement our own partition since itertools.partition might not be available
|
|
423
|
+
objs_without_pk, objs_with_pk = [], []
|
|
424
|
+
for obj in all_child_objects:
|
|
425
|
+
if obj._is_pk_set():
|
|
426
|
+
objs_with_pk.append(obj)
|
|
427
|
+
else:
|
|
428
|
+
objs_without_pk.append(obj)
|
|
429
|
+
|
|
430
|
+
# Use Django's internal _batched_insert method
|
|
431
|
+
opts = child_model._meta
|
|
432
|
+
# For child models in MTI, we need to include the foreign key to the parent
|
|
433
|
+
# but exclude the primary key since it's inherited
|
|
434
|
+
|
|
435
|
+
# Include all local fields except generated ones
|
|
436
|
+
# We need to include the foreign key to the parent (business_ptr)
|
|
437
|
+
fields = [f for f in opts.local_fields if not f.generated]
|
|
438
|
+
|
|
439
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
440
|
+
if objs_with_pk:
|
|
441
|
+
returned_columns = base_qs._batched_insert(
|
|
442
|
+
objs_with_pk,
|
|
443
|
+
fields,
|
|
444
|
+
batch_size=len(objs_with_pk), # Use actual batch size
|
|
445
|
+
)
|
|
446
|
+
for obj_with_pk, results in zip(objs_with_pk, returned_columns):
|
|
447
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
448
|
+
if field != opts.pk:
|
|
449
|
+
setattr(obj_with_pk, field.attname, result)
|
|
450
|
+
for obj_with_pk in objs_with_pk:
|
|
451
|
+
obj_with_pk._state.adding = False
|
|
452
|
+
obj_with_pk._state.db = self.db
|
|
453
|
+
|
|
454
|
+
if objs_without_pk:
|
|
455
|
+
# For objects without PK, we still need to exclude primary key fields
|
|
456
|
+
fields = [
|
|
457
|
+
f
|
|
458
|
+
for f in fields
|
|
459
|
+
if not isinstance(f, AutoField) and not f.primary_key
|
|
460
|
+
]
|
|
461
|
+
returned_columns = base_qs._batched_insert(
|
|
462
|
+
objs_without_pk,
|
|
463
|
+
fields,
|
|
464
|
+
batch_size=len(objs_without_pk), # Use actual batch size
|
|
465
|
+
)
|
|
466
|
+
for obj_without_pk, results in zip(
|
|
467
|
+
objs_without_pk, returned_columns
|
|
468
|
+
):
|
|
469
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
470
|
+
setattr(obj_without_pk, field.attname, result)
|
|
471
|
+
obj_without_pk._state.adding = False
|
|
472
|
+
obj_without_pk._state.db = self.db
|
|
473
|
+
|
|
474
|
+
# Step 3: Update original objects with generated PKs and state
|
|
475
|
+
pk_field_name = child_model._meta.pk.name
|
|
476
|
+
for orig_obj, child_obj in zip(batch, all_child_objects):
|
|
477
|
+
child_pk = getattr(child_obj, pk_field_name)
|
|
478
|
+
setattr(orig_obj, pk_field_name, child_pk)
|
|
479
|
+
orig_obj._state.adding = False
|
|
480
|
+
orig_obj._state.db = self.db
|
|
481
|
+
|
|
482
|
+
return batch
|
|
483
|
+
|
|
484
|
+
def _create_parent_instance(self, source_obj, parent_model, current_parent):
|
|
485
|
+
parent_obj = parent_model()
|
|
486
|
+
for field in parent_model._meta.local_fields:
|
|
487
|
+
# Only copy if the field exists on the source and is not None
|
|
488
|
+
if hasattr(source_obj, field.name):
|
|
489
|
+
value = getattr(source_obj, field.name, None)
|
|
490
|
+
if value is not None:
|
|
491
|
+
setattr(parent_obj, field.name, value)
|
|
492
|
+
if current_parent is not None:
|
|
493
|
+
for field in parent_model._meta.local_fields:
|
|
494
|
+
if (
|
|
495
|
+
hasattr(field, "remote_field")
|
|
496
|
+
and field.remote_field
|
|
497
|
+
and field.remote_field.model == current_parent.__class__
|
|
498
|
+
):
|
|
499
|
+
setattr(parent_obj, field.name, current_parent)
|
|
500
|
+
break
|
|
501
|
+
|
|
502
|
+
# Handle auto_now_add and auto_now fields like Django does
|
|
503
|
+
for field in parent_model._meta.local_fields:
|
|
504
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
505
|
+
# Ensure auto_now_add fields are properly set
|
|
506
|
+
if getattr(parent_obj, field.name) is None:
|
|
507
|
+
field.pre_save(parent_obj, add=True)
|
|
508
|
+
# Explicitly set the value to ensure it's not None
|
|
509
|
+
setattr(parent_obj, field.name, field.value_from_object(parent_obj))
|
|
510
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
511
|
+
field.pre_save(parent_obj, add=True)
|
|
512
|
+
|
|
513
|
+
return parent_obj
|
|
514
|
+
|
|
515
|
+
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
516
|
+
child_obj = child_model()
|
|
517
|
+
# Only copy fields that exist in the child model's local fields
|
|
518
|
+
for field in child_model._meta.local_fields:
|
|
519
|
+
if isinstance(field, AutoField):
|
|
520
|
+
continue
|
|
521
|
+
if hasattr(source_obj, field.name):
|
|
522
|
+
value = getattr(source_obj, field.name, None)
|
|
523
|
+
if value is not None:
|
|
524
|
+
setattr(child_obj, field.name, value)
|
|
525
|
+
|
|
526
|
+
# Set parent links for MTI
|
|
527
|
+
for parent_model, parent_instance in parent_instances.items():
|
|
528
|
+
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
529
|
+
if parent_link:
|
|
530
|
+
# Set both the foreign key value (the ID) and the object reference
|
|
531
|
+
# This follows Django's pattern in _set_pk_val
|
|
532
|
+
setattr(
|
|
533
|
+
child_obj, parent_link.attname, parent_instance.pk
|
|
534
|
+
) # Set the foreign key value
|
|
535
|
+
setattr(
|
|
536
|
+
child_obj, parent_link.name, parent_instance
|
|
537
|
+
) # Set the object reference
|
|
538
|
+
|
|
539
|
+
# Handle auto_now_add and auto_now fields like Django does
|
|
540
|
+
for field in child_model._meta.local_fields:
|
|
541
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
542
|
+
# Ensure auto_now_add fields are properly set
|
|
543
|
+
if getattr(child_obj, field.name) is None:
|
|
544
|
+
field.pre_save(child_obj, add=True)
|
|
545
|
+
# Explicitly set the value to ensure it's not None
|
|
546
|
+
setattr(child_obj, field.name, field.value_from_object(child_obj))
|
|
547
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
548
|
+
field.pre_save(child_obj, add=True)
|
|
549
|
+
|
|
550
|
+
return child_obj
|
|
551
|
+
|
|
552
|
+
def _mti_bulk_update(self, objs, fields, **kwargs):
|
|
553
|
+
"""
|
|
554
|
+
Custom bulk update implementation for MTI models.
|
|
555
|
+
Updates each table in the inheritance chain efficiently using Django's batch_size.
|
|
556
|
+
"""
|
|
557
|
+
model_cls = self.model
|
|
558
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
559
|
+
|
|
560
|
+
# Remove custom hook kwargs before passing to Django internals
|
|
561
|
+
django_kwargs = {
|
|
562
|
+
k: v
|
|
563
|
+
for k, v in kwargs.items()
|
|
564
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Safety check to prevent infinite recursion
|
|
568
|
+
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
569
|
+
raise ValueError(
|
|
570
|
+
"Inheritance chain too deep - possible infinite recursion detected"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Handle auto_now fields by calling pre_save on objects
|
|
574
|
+
# Check all models in the inheritance chain for auto_now fields
|
|
575
|
+
for obj in objs:
|
|
576
|
+
for model in inheritance_chain:
|
|
577
|
+
for field in model._meta.local_fields:
|
|
578
|
+
if hasattr(field, "auto_now") and field.auto_now:
|
|
579
|
+
field.pre_save(obj, add=False)
|
|
580
|
+
|
|
581
|
+
# Add auto_now fields to the fields list so they get updated in the database
|
|
582
|
+
auto_now_fields = set()
|
|
583
|
+
for model in inheritance_chain:
|
|
584
|
+
for field in model._meta.local_fields:
|
|
585
|
+
if hasattr(field, "auto_now") and field.auto_now:
|
|
586
|
+
auto_now_fields.add(field.name)
|
|
587
|
+
|
|
588
|
+
# Combine original fields with auto_now fields
|
|
589
|
+
all_fields = list(fields) + list(auto_now_fields)
|
|
590
|
+
|
|
591
|
+
# Group fields by model in the inheritance chain
|
|
592
|
+
field_groups = {}
|
|
593
|
+
for field_name in all_fields:
|
|
594
|
+
field = model_cls._meta.get_field(field_name)
|
|
595
|
+
# Find which model in the inheritance chain this field belongs to
|
|
596
|
+
for model in inheritance_chain:
|
|
597
|
+
if field in model._meta.local_fields:
|
|
598
|
+
if model not in field_groups:
|
|
599
|
+
field_groups[model] = []
|
|
600
|
+
field_groups[model].append(field_name)
|
|
601
|
+
break
|
|
602
|
+
|
|
603
|
+
# Process in batches
|
|
604
|
+
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
605
|
+
total_updated = 0
|
|
606
|
+
|
|
607
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
608
|
+
for i in range(0, len(objs), batch_size):
|
|
609
|
+
batch = objs[i : i + batch_size]
|
|
610
|
+
batch_result = self._process_mti_bulk_update_batch(
|
|
611
|
+
batch, field_groups, inheritance_chain, **django_kwargs
|
|
612
|
+
)
|
|
613
|
+
total_updated += batch_result
|
|
614
|
+
|
|
615
|
+
return total_updated
|
|
616
|
+
|
|
617
|
+
def _process_mti_bulk_update_batch(self, batch, field_groups, inheritance_chain, **kwargs):
|
|
618
|
+
"""
|
|
619
|
+
Process a single batch of objects for MTI bulk update.
|
|
620
|
+
Updates each table in the inheritance chain for the batch.
|
|
621
|
+
"""
|
|
622
|
+
total_updated = 0
|
|
623
|
+
|
|
624
|
+
# For MTI, we need to handle parent links correctly
|
|
625
|
+
# The root model (first in chain) has its own PK
|
|
626
|
+
# Child models use the parent link to reference the root PK
|
|
627
|
+
root_model = inheritance_chain[0]
|
|
628
|
+
|
|
629
|
+
# Get the primary keys from the objects
|
|
630
|
+
# If objects have pk set but are not loaded from DB, use those PKs
|
|
631
|
+
root_pks = []
|
|
632
|
+
for obj in batch:
|
|
633
|
+
# Check both pk and id attributes
|
|
634
|
+
pk_value = getattr(obj, 'pk', None)
|
|
635
|
+
if pk_value is None:
|
|
636
|
+
pk_value = getattr(obj, 'id', None)
|
|
637
|
+
|
|
638
|
+
if pk_value is not None:
|
|
639
|
+
root_pks.append(pk_value)
|
|
640
|
+
else:
|
|
641
|
+
continue
|
|
642
|
+
|
|
643
|
+
if not root_pks:
|
|
644
|
+
return 0
|
|
645
|
+
|
|
646
|
+
# Update each table in the inheritance chain
|
|
647
|
+
for model, model_fields in field_groups.items():
|
|
648
|
+
if not model_fields:
|
|
649
|
+
continue
|
|
650
|
+
|
|
651
|
+
if model == inheritance_chain[0]:
|
|
652
|
+
# Root model - use primary keys directly
|
|
653
|
+
pks = root_pks
|
|
654
|
+
filter_field = 'pk'
|
|
655
|
+
else:
|
|
656
|
+
# Child model - use parent link field
|
|
657
|
+
parent_link = None
|
|
658
|
+
for parent_model in inheritance_chain:
|
|
659
|
+
if parent_model in model._meta.parents:
|
|
660
|
+
parent_link = model._meta.parents[parent_model]
|
|
661
|
+
break
|
|
662
|
+
|
|
663
|
+
if parent_link is None:
|
|
664
|
+
continue
|
|
665
|
+
|
|
666
|
+
# For child models, the parent link values should be the same as root PKs
|
|
667
|
+
pks = root_pks
|
|
668
|
+
filter_field = parent_link.attname
|
|
669
|
+
|
|
670
|
+
if pks:
|
|
671
|
+
base_qs = model._base_manager.using(self.db)
|
|
672
|
+
|
|
673
|
+
# Check if records exist
|
|
674
|
+
existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
|
|
675
|
+
|
|
676
|
+
if existing_count == 0:
|
|
677
|
+
continue
|
|
678
|
+
|
|
679
|
+
# Build CASE statements for each field to perform a single bulk update
|
|
680
|
+
case_statements = {}
|
|
681
|
+
for field_name in model_fields:
|
|
682
|
+
field = model._meta.get_field(field_name)
|
|
683
|
+
when_statements = []
|
|
684
|
+
|
|
685
|
+
for pk, obj in zip(pks, batch):
|
|
686
|
+
# Check both pk and id attributes for the object
|
|
687
|
+
obj_pk = getattr(obj, 'pk', None)
|
|
688
|
+
if obj_pk is None:
|
|
689
|
+
obj_pk = getattr(obj, 'id', None)
|
|
690
|
+
|
|
691
|
+
if obj_pk is None:
|
|
692
|
+
continue
|
|
693
|
+
value = getattr(obj, field_name)
|
|
694
|
+
when_statements.append(When(**{filter_field: pk}, then=Value(value, output_field=field)))
|
|
695
|
+
|
|
696
|
+
case_statements[field_name] = Case(*when_statements, output_field=field)
|
|
697
|
+
|
|
698
|
+
# Execute a single bulk update for all objects in this model
|
|
699
|
+
try:
|
|
700
|
+
updated_count = base_qs.filter(**{f"{filter_field}__in": pks}).update(**case_statements)
|
|
701
|
+
total_updated += updated_count
|
|
702
|
+
except Exception as e:
|
|
703
|
+
import traceback
|
|
704
|
+
traceback.print_exc()
|
|
705
|
+
|
|
706
|
+
return total_updated
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class HookQuerySet(HookQuerySetMixin, models.QuerySet):
|
|
710
|
+
"""
|
|
711
|
+
A QuerySet that provides bulk hook functionality.
|
|
712
|
+
This is the traditional approach for backward compatibility.
|
|
713
|
+
"""
|
|
714
|
+
pass
|