ModelSerializer 高階使用

雲崖先生發表於2020-11-03

前言

   ModelSerializer中還具有一些高階用法,如批量更新、批量刪除、批量建立等。

   但是批量過來的資料格式都需要與前端做好協商,什麼樣的資料格式是單條操作,什麼樣的資料格式是批量操作。

   如下,對於單條操作而言,只需要傳入編號即可,而批量操作則需要在請求體中傳入[]以及被操作的主鍵編號。

模型表

   書籍表、出版社表、作者表、作者詳情表

   作者和作者詳情表是一對一關係

   書籍和出版社表是一對多關係

   書籍和作者是多對多關係

   以上關係均是為ORM查詢方便所提供,但是在物理表中並沒有確切關聯

   由於這些表中的資料具有商業價值,所以我們要給它設定一個欄位名為delete_status,代表是否邏輯刪除(真實並不會刪除)。

   初此之外還有create_time代表該記錄的建立時間,以及last_update_time代表這張表的最後更新時間。

   所以我們可以給這三張欄位抽出一張抽象表,用於被其他表繼承:

from django.db import models

# Create your models here.
class BaseModel(models.Model):
    delete_status = models.BooleanField(default=False)  # 預設不刪除
    create_time = models.DateTimeField(auto_now_add=True)  # 新增記錄時自動插入
    last_update_time = models.DateTimeField(auto_now=True)  # 更新記錄時自動插入

    class Meta:
        abstract = True  # 抽象類,不會建立真實表,用於繼承


class Book(BaseModel):
    book_id = models.AutoField(primary_key=True)
    book_name = models.CharField(max_length=32,verbose_name="書籍名稱")
    book_price = models.DecimalField(max_digits=5,decimal_places=2,verbose_name="書籍價格")
    publish = models.ForeignKey(to="Publish",on_delete=models.DO_NOTHING,db_constraint=False)
    # 邏輯一對多,實際表沒有關聯,刪除出版社書不受影響
    authors = models.ManyToManyField(to="Author",db_constraint=False)
    # 邏輯多對多,實際表沒有任何關聯
    class Meta:
        verbose_name_plural = "書籍表"

    def __str__(self):
        return self.book_name

    @property
    def publish_name(self):  # 模擬欄位,用於序列化時顯示出版社名稱
        return self.publish.publish_name

    @property
    def author_list(self):  # 模擬欄位,用於序列化時顯示多個作者的名字和性別
        author_list = self.authors.all()

        return [
            {"author_name":author.author_name,"author_gender":author.get_author_gender_display()} for author in author_list
        ]


class Publish(BaseModel):
    publish_id = models.AutoField(primary_key=True)
    publish_name = models.CharField(max_length=32,verbose_name="出版社名稱")
    publish_addr = models.CharField(max_length=32,verbose_name="出版社地址")

    def __str__(self):
        return self.publish_name


class Author(BaseModel):
    author_id = models.AutoField(primary_key=True)
    author_name = models.CharField(max_length=32,verbose_name="作者姓名")
    author_gender = models.IntegerField(choices=[(1,"男"),(2,"女")])
    author_detail = models.OneToOneField(to="AuthorDetail",db_constraint=False,on_delete=models.CASCADE)
    # 邏輯一對一,實際上沒有什麼聯絡

class AuthorDetail(BaseModel):
    author_phone = models.CharField(max_length=11)



# 二、表斷關聯
# 1、表之間沒有外來鍵關聯,但是有外來鍵邏輯關聯(有充當外來鍵的欄位)
# 2、斷關聯後不會影響資料庫查詢效率,但是會極大提高資料庫增刪改效率(不影響增刪改查操作)
# 3、斷關聯一定要通過邏輯保證表之間資料的安全,不要出現髒資料,程式碼控制
# 4、斷關聯
# 5、級聯關係
#       作者沒了,詳情也沒:on_delete=models.CASCADE
#       出版社沒了,書還是那個出版社出版:on_delete=models.DO_NOTHING
#       部門沒了,員工沒有部門(空不能):null=True, on_delete=models.SET_NULL
#       部門沒了,員工進入預設部門(預設值):default=0, on_delete=models.SET_DEFAULT

詳解序列器

   進行序列化時,如何區分是建立、更新以及查詢單條或多條呢?這其實涉及到ModelSerializer的引數問題。

def __init__(self, instance=None, data=empty, **kwargs):

   如果只傳入data引數,代表這是新增一條,則情況如下:

方法\屬性\鉤子狀態
is_valid() 可用
initial_data() 可用
validated_data 僅在呼叫is_valid()之後可用
errors 僅在呼叫is_valid()之後可用
data 僅在呼叫is_valid()之後可用

   如果沒有傳遞data引數,則會發生如下情況:

方法\屬性\鉤子狀態
is_valid() 不可用
initial_data() 不可用
validated_data 不可用
errors 不可用
data 可用

   我們要注意傳遞引數的情況如下:

   查詢單條,只會傳入instance

   建立一條,只會傳入data

   更新一條,會傳入instance以及data

   上面沒有涉及到查詢多條,那麼在查詢多條時我們會傳遞進many引數,在內部會執行這樣一段程式碼:

    def __new__(cls, *args, **kwargs):
        # We override this method in order to automatically create
        # `ListSerializer` classes instead when `many=True` is set.
        if kwargs.pop('many', False):
            return cls.many_init(*args, **kwargs)
        return super().__new__(cls, *args, **kwargs)

   這句程式碼的意思是,當有manyTrue時代表這是多條操作,它將不會例項化你的自定義序列化器,因為你的自定義序列化器都是針對單條記錄的,轉而它會例項化一個叫做ListSerializer的類,該類是內建的,並且該類支援批量建立。

   下面是ListSerializer的部分原始碼,其中child代表你自定義的序列化器:

class ListSerializer(BaseSerializer):
    child = None  # 代表你自己寫的序列化器
    many = True  

    default_error_messages = {
        'not_a_list': _('Expected a list of items but got type "{input_type}".'),
        'empty': _('This list may not be empty.')
    }

    def __init__(self, *args, **kwargs):
        self.child = kwargs.pop('child', copy.deepcopy(self.child))  # 自己寫的序列化器
        self.allow_empty = kwargs.pop('allow_empty', True)
        assert self.child is not None, '`child` is a required argument.'
        assert not inspect.isclass(self.child), '`child` has not been instantiated.'
        super().__init__(*args, **kwargs)
        self.child.bind(field_name='', parent=self)

   繼續向下看它的原始碼,可以發現它是支援批量建立的,但是不支援批量更新。

    def update(self, instance, validated_data):
        raise NotImplementedError(
            "Serializers with many=True do not support multiple update by "
            "default, only multiple create. For updates it is unclear how to "
            "deal with insertions and deletions. If you need to support "
            "multiple update, use a `ListSerializer` class and override "
            "`.update()` so you can specify the behavior exactly."
        )

    def create(self, validated_data):
        return [
            self.child.create(attrs) for attrs in validated_data
        ]

   如果我們對自己的序列化器做一個ListSerializer,則可以繼承原生的ListSerializer,並且指定好當有many引數傳遞時,例項化的是我們自己寫的ListSerializer即可。如下所示:

from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import ListSerializer
from app01 import models

# 寫一個類,繼承ListSerializer,重寫update方法實現批量更新
class BookListSerializer(ListSerializer):
    def update(self,instance,validated_data):

        return [
            self.child.update(instance[i],attrs) for i,attrs in enumerate(validated_data)
        ]

class BookModelSerializer(ModelSerializer):
    class Meta:
        list_serializer_class = BookListSerializer  # 使用many引數後後用指定的類進行例項化
        model = models.Book

        # depth=0 # 序列化的關聯層級
        fields = ("pk","book_name","book_price","authors","publish","publish_name","author_list")
        # 使用property來拿到展示的資料

        extra_kwargs = {
            "publish":{"write_only":True},  # 不展示,但是新增或更新需要指定
            "publish_name":{"read_only":True},  # 僅用於展示
            "authors":{"write_only":True},
            "author_list":{"read_only":True},
        }

   總結如下:

   1.我們自己寫的序列化器都只能對單條進行操作

   2.如果傳入many為True時,則會自動序列化一個內部的ListSerializer類,它支援批量建立多條,但是不支援批量更新

   3.在自定義序列器中使用list_serializer_class類屬性即可指定在進行批量操縱時用哪一個類進行例項化

序列化與反序列化

   檢視上面的程式碼,你可以發現在model中的很多地方都用了property來對例項方法進行裝飾。

   它其實是為了配合序列化做的,因為我們在自定以序列化器中的fields屬性裡將他們進行加入了,如下所示:

    @property
    def publish_name(self):  # 模擬欄位,用於序列化時顯示出版社名稱
        return self.publish.publish_name

	@property
    def author_list(self):  # 模擬欄位,用於序列化時顯示多個作者的名字和性別
        author_list = self.authors.all()

        return [
            {"author_name":author.author_name,"author_gender":author.get_author_gender_display()} for author in author_list
        ]
        
    fields = ("pk","book_name","book_price","authors","publish","publish_name","author_list")  # 返回外來鍵,出版社的名字,返回多對多外來鍵,作者的列表

   並且,還指定了extra_kwargs引數,用於指定哪些引數是序列化時用,哪些引數是反序列化時用:

        extra_kwargs = {
            "publish":{"write_only":True},  # 不展示,但是新增或更新需要指定
            "publish_name":{"read_only":True},  # 僅用於展示
            "authors":{"write_only":True},
            "author_list":{"read_only":True},
        }

   這其實也要與前端溝通好,我序列化丟給你的資料是字串,但是你要建立時必須給我把諸如出版社、作者等資訊的pk放進來才行。

   大概意思就是,我丟給你字串讓使用者看,你建立或更新時我不要字串,我要pk主鍵。

   此外,還有一個引數叫做depth,它規定了在序列化時是否連同外來鍵一起進行序列化,並且序列化的層級是多少,一般看一看就行了。

介面書寫

   規定,對於單條操作,直接放在?請求地址後面。放入pk即可。

   多條操作,你需要放入請求body中,以[]形式進行傳遞。

from rest_framework.generics import GenericAPIView
from rest_framework.mixins import CreateModelMixin,ListModelMixin,RetrieveModelMixin,UpdateModelMixin
from rest_framework.response import Response

from app01.serializer import *
from app01 import models


class BookAPI(GenericAPIView,CreateModelMixin,ListModelMixin,RetrieveModelMixin,UpdateModelMixin):
    queryset = models.Book.objects.filter(delete_status=False)  # 查詢未刪除的資料
    serializer_class = BookModelSerializer

    def get(self,request,*args,**kwargs):
        pk = kwargs.get("pk")
        if not pk:  # 獲取所有
            return self.list(request)
        # 獲取單條
        return self.retrieve(request,pk)

    def post(self,request,*args,**kwargs):
        # 新增一條
        if isinstance(request.data,dict):
            return self.create(request)  # 自動返回

        # 新增多條
        elif isinstance(request.data,list):
            # 現在執行我們自己定義的ListSerializer,因為傳入many=True.由於繼承原生的ListSerializer,它自己有create方法
            book_ser = self.get_serializer(data=request.data,many=True)
            book_ser.is_valid(raise_exception=True) # 序列化失敗直接丟擲異常
            book_ser.save()
            return Response(data=book_ser.data)

    def patch(self, request, *args, **kwargs):
        pk = kwargs.get("pk")
        print(pk)
        if pk:
            return self.update(request,pk)
        # 改多個
        # 前端傳遞資料格式[{book_id:1,book_name:xx,book_price:xx},{book_id:1,book_name:xx,book_price:xx}]
        # 處理傳入的資料  物件列表[book1,book2]  修改的資料列表[{book_name:xx,book_price:xx},{book_name:xx,book_price:xx}]
        book_list = []
        modify_data = []
        for item in request.data:
            # {book_id:1,book_name:xx,book_price:xx} 取出pk,不允許修改pk
            pk = item.pop("pk")
            book_obj = models.Book.objects.get(pk=pk)
            book_list.append(book_obj)
            modify_data.append(item)

        book_ser = BookModelSerializer(instance=book_list,data=modify_data,many=True,partial=True)  # parital允許區域性修改,這個主要針對put,patch本身就是True
        # 處理時:
        # self.child.update(instance[i],attrs) for i,attrs in enumerate(validated_data)  傳入id,和要修改的資料
        book_ser.is_valid(raise_exception=True)
        book_ser.save()
        return Response(book_ser.data)

    def delete(self,request,*args,**kwargs):
        pk = kwargs.get("pk")
        pks = []  # 用於獲取要刪除的id,全部放入列表中
        if pk:
            # 刪一個
            pks.append(pk)
        else:
            pks = request.data.get("pks")

        result = models.Book.objects.filter(pk__in=pks,delete_status=False).update(delete_status=True)
        if result:
            return Response(data="刪除%s條記錄成功"%len(pks))
        else:
            return Response(data="刪除失敗,沒有要刪除的資料")

資料格式

   新增資料:

# 新增一條 http://127.0.0.1:8000/api/books/  POST請求
# 請求的資料格式:

{
    "book_name":"新書,單條",
    "book_price": "123.00",
    "publish": 1,
    "authors": [
        1,2
    ]
}

# 返回格式:

{
    "pk": 12,
    "book_name": "新書,單條",
    "book_price": "123.00",
    "publish_name": "北京出版社",
    "author_list": [
        {
            "author_name": "雲崖",
            "author_gender": "男"
        },
        {
            "author_name": "小屁孩",
            "author_gender": "女"
        }
    ]
}

# 新增多條 http://127.0.0.1:8000/api/books/  POST請求
# 請求的資料格式:

[
    {
    "book_name":"新書1,多條",
    "book_price": "88.00",
    "publish": 1,
    "authors": [
        1
    ]
},
{
    "book_name":"新書2,多條",
    "book_price": "13.00",
    "publish": 1,
    "authors": [
        2,3
    ]
},
{
    "book_name":"新書3,多條",
    "book_price": "63.00",
    "publish": 1,
    "authors": [
        3,4
    ]
}
]

# 返回格式:

[
    {
        "pk": 18,
        "book_name": "新書1,多條",
        "book_price": "88.00",
        "publish_name": "北京出版社",
        "author_list": [
            {
                "author_name": "雲崖",
                "author_gender": "男"
            }
        ]
    },
    {
        "pk": 19,
        "book_name": "新書2,多條",
        "book_price": "13.00",
        "publish_name": "北京出版社",
        "author_list": [
            {
                "author_name": "小屁孩",
                "author_gender": "女"
            },
            {
                "author_name": "東仙人",
                "author_gender": "男"
            }
        ]
    },
    {
        "pk": 20,
        "book_name": "新書3,多條",
        "book_price": "63.00",
        "publish_name": "北京出版社",
        "author_list": [
            {
                "author_name": "東仙人",
                "author_gender": "男"
            },
            {
                "author_name": "雲散人",
                "author_gender": "女"
            }
        ]
    }
]

   修改資料:

# 修改一條 http://127.0.0.1:8000/api/books/1/  patch請求
# 請求的資料格式:
{
    "book_name":"修改新書",
    "book_price": "123.00",
    "publish": 3,
    "authors": [
        2,3
    ]
}

# 返回格式:
{
    "pk": 1,
    "book_name": "修改新書",
    "book_price": "123.00",
    "publish_name": "西京出版社",
    "author_list": [
        {
            "author_name": "小屁孩",
            "author_gender": "女"
        },
        {
            "author_name": "東仙人",
            "author_gender": "男"
        }
    ]
}

# 修改多條 http://127.0.0.1:8000/api/books/  patch請求
# 請求的資料格式:
[
    {
    "pk": 1, 
    "book_name":"修改新書",
    "book_price": "123.00",
    "publish": 1,
    "authors": [
        3,4
    ]
	},
	{
    "pk": 2,
    "book_name":"修改新書2",
    "book_price": "123.00",
    "publish": 1,
    "authors": [
        1,2
    ]
	}
]

# 返回格式:
[
    {
        "pk": 1,
        "book_name": "修改新書",
        "book_price": "123.00",
        "publish_name": "北京出版社",
        "author_list": [
            {
                "author_name": "東仙人",
                "author_gender": "男"
            },
            {
                "author_name": "雲散人",
                "author_gender": "女"
            }
        ]
    },
    {
        "pk": 2,
        "book_name": "修改新書2",
        "book_price": "123.00",
        "publish_name": "北京出版社",
        "author_list": [
            {
                "author_name": "雲崖",
                "author_gender": "男"
            },
            {
                "author_name": "小屁孩",
                "author_gender": "女"
            }
        ]
    }
]

   刪除資料:

# 刪除一條 http://127.0.0.1:8000/api/books/1/  delete請求

# 刪除多條 http://127.0.0.1:8000/api/books/  delete請求
# 請求資料格式:
{
    "pks":
    [1,2,3]
}

相關文章