0
# Model Mixins and Advanced Features
1
2
Model mixins for enhanced FSM functionality including refresh_from_db support for protected fields and optimistic locking protection against concurrent transitions.
3
4
## Capabilities
5
6
### FSMModelMixin
7
8
Mixin that allows refresh_from_db for models with fsm protected fields. This mixin ensures that protected FSM fields are properly handled during database refresh operations.
9
10
```python { .api }
11
class FSMModelMixin(object):
12
def refresh_from_db(self, *args, **kwargs):
13
"""
14
Refresh model instance from database while respecting protected FSM fields.
15
16
Protected FSM fields are excluded from refresh to prevent corruption
17
of the finite state machine's integrity.
18
"""
19
20
def _get_protected_fsm_fields(self):
21
"""
22
Get set of protected FSM field attribute names.
23
24
Returns:
25
set: Attribute names of protected FSM fields
26
"""
27
```
28
29
Usage example:
30
31
```python
32
from django.db import models
33
from django_fsm import FSMField, FSMModelMixin, transition
34
35
class Order(FSMModelMixin, models.Model):
36
state = FSMField(default='pending', protected=True)
37
amount = models.DecimalField(max_digits=10, decimal_places=2)
38
39
@transition(field=state, source='pending', target='confirmed')
40
def confirm(self):
41
pass
42
43
# Protected fields are automatically excluded during refresh
44
order = Order.objects.get(pk=1)
45
order.refresh_from_db() # state field is preserved if protected
46
```
47
48
### ConcurrentTransitionMixin
49
50
Protects models from undesirable effects caused by concurrently executed transitions using optimistic locking. This mixin prevents race conditions where multiple processes try to modify the same object's state simultaneously.
51
52
```python { .api }
53
class ConcurrentTransitionMixin(object):
54
def __init__(self, *args, **kwargs):
55
"""Initialize concurrent transition protection."""
56
57
def save(self, *args, **kwargs):
58
"""
59
Save model with concurrent transition protection.
60
61
Raises:
62
ConcurrentTransition: If state has changed since object was fetched
63
"""
64
65
def refresh_from_db(self, *args, **kwargs):
66
"""Refresh from database and update internal state tracking."""
67
68
@property
69
def state_fields(self):
70
"""
71
Get all FSM fields in the model.
72
73
Returns:
74
filter: FSM field instances
75
"""
76
77
def _update_initial_state(self):
78
"""Update internal tracking of initial state values."""
79
80
def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
81
"""
82
Internal method that performs the actual database update with state validation.
83
84
This method is called by Django's save mechanism and adds concurrent
85
transition protection by filtering the update query on the original
86
state values.
87
88
Parameters:
89
- base_qs: Base queryset for the update
90
- using: Database alias to use
91
- pk_val: Primary key value
92
- values: Values to update
93
- update_fields: Fields to update
94
- forced_update: Whether update is forced
95
96
Returns:
97
bool: True if update was successful
98
99
Raises:
100
ConcurrentTransition: If state has changed since object was fetched
101
"""
102
```
103
104
Usage example:
105
106
```python
107
from django.db import transaction
108
from django_fsm import FSMField, ConcurrentTransitionMixin, transition, ConcurrentTransition
109
110
class BankAccount(ConcurrentTransitionMixin, models.Model):
111
state = FSMField(default='active')
112
balance = models.DecimalField(max_digits=10, decimal_places=2)
113
114
@transition(field=state, source='active', target='frozen')
115
def freeze_account(self):
116
pass
117
118
# Safe concurrent usage pattern
119
def transfer_money(account_id, amount):
120
try:
121
with transaction.atomic():
122
account = BankAccount.objects.get(pk=account_id)
123
if account.balance >= amount:
124
account.balance -= amount
125
account.save() # Will raise ConcurrentTransition if state changed
126
except ConcurrentTransition:
127
# Handle concurrent modification
128
raise ValueError("Account was modified by another process")
129
```
130
131
## Advanced Integration Patterns
132
133
### Combining Both Mixins
134
135
You can use both mixins together for maximum protection:
136
137
```python
138
class SafeDocument(FSMModelMixin, ConcurrentTransitionMixin, models.Model):
139
state = FSMField(default='draft', protected=True)
140
workflow_state = FSMField(default='new')
141
content = models.TextField()
142
143
@transition(field=state, source='draft', target='published')
144
def publish(self):
145
pass
146
147
@transition(field=workflow_state, source='new', target='processing')
148
def start_workflow(self):
149
pass
150
```
151
152
### Custom Concurrent Protection
153
154
Customize which fields are used for concurrent protection:
155
156
```python
157
class CustomProtectedModel(ConcurrentTransitionMixin, models.Model):
158
primary_state = FSMField(default='new')
159
secondary_state = FSMField(default='inactive')
160
version = models.PositiveIntegerField(default=1)
161
162
def save(self, *args, **kwargs):
163
# Custom logic before concurrent protection
164
if self.primary_state == 'published':
165
self.version += 1
166
167
# Call parent save with concurrent protection
168
super().save(*args, **kwargs)
169
```
170
171
### Handling Concurrent Exceptions
172
173
Proper exception handling for concurrent modifications:
174
175
```python
176
from django_fsm import ConcurrentTransition
177
import time
178
import random
179
180
def safe_state_change(model_instance, transition_method, max_retries=3):
181
"""
182
Safely execute state transition with retry logic for concurrent conflicts.
183
"""
184
for attempt in range(max_retries):
185
try:
186
with transaction.atomic():
187
# Refresh to get latest state
188
model_instance.refresh_from_db()
189
190
# Execute transition
191
transition_method()
192
model_instance.save()
193
return True
194
195
except ConcurrentTransition:
196
if attempt == max_retries - 1:
197
raise
198
199
# Wait before retry with exponential backoff
200
time.sleep(0.1 * (2 ** attempt) + random.uniform(0, 0.1))
201
202
return False
203
204
# Usage
205
try:
206
safe_state_change(order, order.confirm)
207
except ConcurrentTransition:
208
# Final failure after retries
209
logger.error(f"Failed to confirm order {order.id} after retries")
210
```
211
212
### Integration with Django Signals
213
214
Combining mixins with Django FSM signals for comprehensive state management:
215
216
```python
217
from django_fsm.signals import post_transition
218
from django.dispatch import receiver
219
220
class AuditedOrder(FSMModelMixin, ConcurrentTransitionMixin, models.Model):
221
state = FSMField(default='pending', protected=True)
222
audit_trail = models.JSONField(default=list)
223
224
@transition(field=state, source='pending', target='confirmed')
225
def confirm(self):
226
pass
227
228
@receiver(post_transition, sender=AuditedOrder)
229
def log_state_change(sender, instance, name, source, target, **kwargs):
230
"""Log all state changes for audit purposes."""
231
instance.audit_trail.append({
232
'timestamp': timezone.now().isoformat(),
233
'transition': name,
234
'from_state': source,
235
'to_state': target,
236
'user_id': getattr(kwargs.get('user'), 'id', None)
237
})
238
239
# Save without triggering state machine (direct field update)
240
sender.objects.filter(pk=instance.pk).update(
241
audit_trail=instance.audit_trail
242
)
243
```
244
245
## Performance Considerations
246
247
### Optimizing Protected Field Queries
248
249
When using FSMModelMixin with many protected fields:
250
251
```python
252
class OptimizedModel(FSMModelMixin, models.Model):
253
state = FSMField(default='new', protected=True)
254
workflow = FSMField(default='pending', protected=True)
255
256
# Use select_related/prefetch_related for efficient queries
257
@classmethod
258
def get_with_relations(cls, pk):
259
return cls.objects.select_related('related_field').get(pk=pk)
260
```
261
262
### Batch Operations with Concurrent Protection
263
264
Handle bulk operations safely:
265
266
```python
267
def bulk_state_update(queryset, transition_method_name):
268
"""
269
Safely update multiple objects with concurrent protection.
270
"""
271
updated_count = 0
272
273
for obj in queryset:
274
try:
275
with transaction.atomic():
276
obj.refresh_from_db()
277
method = getattr(obj, transition_method_name)
278
method()
279
obj.save()
280
updated_count += 1
281
except ConcurrentTransition:
282
# Log failure but continue with other objects
283
logger.warning(f"Concurrent update conflict for {obj.pk}")
284
285
return updated_count
286
```
287
288
### Database Constraints and State Consistency
289
290
Ensure database-level consistency:
291
292
```python
293
class ConstrainedOrder(ConcurrentTransitionMixin, models.Model):
294
state = FSMField(default='pending')
295
payment_confirmed = models.BooleanField(default=False)
296
297
class Meta:
298
constraints = [
299
models.CheckConstraint(
300
check=~(models.Q(state='shipped') & models.Q(payment_confirmed=False)),
301
name='shipped_orders_must_be_paid'
302
)
303
]
304
305
@transition(field=state, source='confirmed', target='shipped')
306
def ship(self):
307
if not self.payment_confirmed:
308
raise ValueError("Cannot ship unpaid order")
309
```