(十三)书籍、关联与修改记录的同一事务
保存书籍之后,还需要写标签关联和一条“新增”或“修改”记录。这几步应一起成功。
# 续写同一文件:reading_project/src/library/models.py
class BookChange(models.Model):
# 一次保存新建一条历史记录,删除书时随所属书籍一起删除。
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="changes")
action = models.CharField(
"动作", max_length=10, choices=[("created", "新增"), ("updated", "修改")]
)
created_at = models.DateTimeField("记录时间", auto_now_add=True)
class Meta:
ordering = ["pk"]BookChange.book 关联一本书,related_name="changes" 让详情页用 book.changes.all() 读取历史;删除书籍时修改记录一并删除。choices 给数据库动作值配显示名称,auto_now_add 只记录首次创建时间。
# 项目文件:reading_project/src/library/service.py
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404
from .models import Book, BookChange, Tag
def owned_book(user, pk):
book = get_object_or_404(
Book.objects.select_related("owner").prefetch_related("tags", "changes"), pk=pk
)
if book.owner_id != user.pk:
raise PermissionDenied("这本书属于其他用户")
return book
def save_book(form, user):
# 一次业务保存包括书籍、标签关联和修改记录,任何一步失败都回滚。
with transaction.atomic():
book = form.save(commit=False)
if book.pk and book.owner_id != user.pk:
raise PermissionDenied
action = "updated" if book.pk else "created"
book.owner = user
book.save()
tags = [
Tag.objects.get_or_create(name=name)[0]
for name in form.cleaned_data["tags_text"]
]
book.tags.set(tags)
BookChange.objects.create(book=book, action=action)
return book
def public_book(book):
return {
"id": book.pk,
"title": book.title,
"finished": book.finished,
"notes": book.notes,
"tags": [tag.name for tag in book.tags.all()],
}transaction.atomic 把保存书、建立标签、替换关联和写修改记录包在一起。末步失败时,前面已经执行的 SQL 也会回滚。异常在 atomic 外处理,之后重新查询真实记录。
transaction_steps.py 在最后一步故意抛出异常。书籍和新标签都查不到,但 Python 对象中已经取得的 pk 仍然存在:数据库回滚不会倒放普通 Python 属性赋值,后续判断以重新查询的持久化状态为准。