Python 现代开发学习笔记

(十三)书籍、关联与修改记录的同一事务

保存书籍之后,还需要写标签关联和一条“新增”或“修改”记录。这几步应一起成功。

# 续写同一文件: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 属性赋值,后续判断以重新查询的持久化状态为准。

事务内先插入书籍取得编号,再建立标签与关系,最后记录失败导致数据库全部回滚,而内存对象的编号没有自动清空

笔记正文与全部题解 · Ctrl / ⌘ K