一. 前言
在开发REST API接口时,视图中做的最主要有三件事:
- 将请求的数据(如JSON格式)转换为模型类对象
- 操作数据库
- 将模型类对象转换为响应的数据(如JSON格式)
序列化:
将程序中的一个数据结构类型转换为其他格式(字典、JSON、XML等),例如将Django中的模型类对象装换为JSON字符串,这个转换过程我们称为序列化。
简单的一句话理解就是将数据转化为JSON格式返回给前端
反序列化:
反之,将其他格式(字典、JSON、XML等)转换为程序中的数据,例如将JSON字符串转换为Django中的模型类对象,这个过程我们称为反序列化。
总结
在开发REST API时,视图中要频繁的进行序列化与反序列化的编写。
在开发REST API接口时,我们在视图中需要做的最核心的事是:
- 将数据库数据序列化为前端所需要的格式,并返回;
- 将前端发送的数据反序列化为模型类对象,并保存到数据库中。
二. 环境安装配置
1. 安装DRF
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple djangorestframework
2. 添加rest_framework应用
INSTALLED_APPS = [
...
'rest_framework',
]
接下来就可以使用DRF进行开发了。
普通的序列化器这里就不介绍了,下面介绍的时进阶版的模型类序列化器-ModelSerializer
此项目使用的时前后端不分离的开发模式,本质上给前端传递数据是一样的
三. 序列化与反序列化
模型表models.py
from django.contrib.auth.models import User
from django.db import models
from apps.file.models import TeamDataStructureFile
from utils.base_models import BaseModel
class DataStructure(BaseModel):
CATEGORY_CHOICES = [
('Knowledge Category', 'Knowledge Category'),
('Knowledge Category1', 'Knowledge Category1'),
]
TEAM_CHOICES = [
('sys-tech', 'sys-tech'),
('data structure', 'data structure'),
]
title = models.CharField(max_length=200, verbose_name='主题', blank=False)
owner = models.CharField(max_length=50,
blank=True,
verbose_name='申请人ID')
full_name = models.CharField(max_length=50,
blank=False,
verbose_name='申请人')
knowledge_category = models.CharField(max_length=50,
blank=False,
choices=CATEGORY_CHOICES,
verbose_name='知识类别')
team = models.CharField(max_length=50,
blank=False,
choices=TEAM_CHOICES,
verbose_name='团队')
description = models.TextField(null=True, blank=True, verbose_name='描述')
# 添加user, file复合主键
# 一对多
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
# 多对多
attachments = models.ManyToManyField(TeamDataStructureFile, blank=True)
class Meta:
ordering = ['-create_time']
def __str__(self):
return self.title
序列化器serializers.py
from rest_framework.serializers import ModelSerializer
from apps.file.models import TeamDataStructureFile
from apps.file.serializers import TeamDataStructureFileField
from teams.models import DataStructure
class DataStructureSerializer(ModelSerializer):
"""序列化与反序列化数据时可以使用"""
class Meta:
model = DataStructure
fields = '__all__'
class EditDataStructureSerializer(ModelSerializer):
"""编辑数据反序列化器"""
class Meta:
model = DataStructure
exclude = ('attachments',) # 若编辑文件接口提交时文件为空,此时不需要序列化此字段
class TeamDataStructureQuerySerializer(ModelSerializer):
"""查询所有数据的序列化器"""
attachments = TeamDataStructureFileField(queryset=TeamDataStructureFile.objects.all(), many=True)
class Meta:
model = DataStructure
fields = '__all__'
1.添加数据
视图函数views.py
class AddDataStructureView(LoginRequiredJSONMixin, APIView):
def get(self, request):
ds = DataStructure()
knowledge_category = [i[1] for i in ds.CATEGORY_CHOICES]
teams = [i[1] for i in ds.TEAM_CHOICES]
context = {
'add_knowledge_category': knowledge_category,
'add_teams': teams,
}
return render(request, 'teams/data_structure/add_data_structure.html', context)
@transaction.atomic
def post(self, request):
"""反序列化一条数据,存入数据库"""
# print(request.data)
serializer = DataStructureSerializer(data=request.data)
if serializer.is_valid():
# serializer.
ds = serializer.save()
# handle file
files_obj = request.FILES.getlist('uploadFile')
if files_obj:
handle_files(request, files_obj, ds, TeamFileSerializer)
return api_success('信息保存成功!Data loading')
return api_bad_request('表单数据输入有误,认证失败,数据无法保存!')
2.查询/编辑/删除数据
class DataStructureDetailView(LoginRequiredJSONMixin, APIView):
def get(self, request, id):
"""序列化器数据,并返回给前端"""
ds = get_object_or_404(DataStructure, pk=id)
s = DataStructureSerializer(ds)
context = s.data
context['teams'] = [i[1] for i in ds.TEAM_CHOICES if i[1] != s.data['team']]
context['knowledge_categories'] = [i[1] for i in ds.CATEGORY_CHOICES if i[1] !=
s.data['knowledge_category']]
attachments = ds.attachments.all()
initialPreviewData = []
try:
for file in attachments:
initialPreviewData.append({
'file_id': file.id,
'file_url': SERVER_URL + '/' + file.file.url,
'file_type': file.suffix.lower(),
'file_name': file.filename,
'file_size': file.file.size
})
except Exception as e:
logger.info('get DataStructureDetailView error:{}'.format(e))
context['initialPreviewData'] = initialPreviewData
context['user'] = ds.user
# print(context)
return render(request, 'teams/data_structure/edit_data_structure.html', context)
@transaction.atomic
def put(self, request, id):
""""反序列化数据,存入数据库"""
# print(request.data)
ds = get_object_or_404(DataStructure, pk=id)
old_ds = copy.copy(ds)
# 此处重新定义了序列化器是因为文件传过来的时候为空,所以需要重新定义新的序列化器,先处理数据,再处理文件,exclude attachments字段可使该字段在处理数据时被忽略,最后在单独处理该字段
s = EditDataStructureSerializer(instance=ds, data=request.data)
if s.is_valid():
new_ds = s.save()
# handle file
files_obj = request.FILES.getlist('uploadFile')
if files_obj:
handle_files(request, files_obj, new_ds, TeamFileSerializer)
# 变更差异信息
# old_ds_dic = model_to_dict(old_ds)
# new_ds_dic = model_to_dict(new_ds)
# diff = old_ds_dic.keys() & new_ds_dic
# diff_vals = [(k + ': from ' + str(old_ds_dic[k]) + ' to ' + str(new_ds_dic[k])) for k in diff if
# old_ds_dic[k] != new_ds_dic[k]]
# print(diff_vals)
return api_success('信息保存成功!Data loading')
return api_bad_request('数据表单验证失败,无法保存!')
def delete(self, request, id):
# print(request, id)
ds = get_object_or_404(DataStructure, pk=id)
res = delete_data(ds)
if res:
return api_success(res)
return api_bad_request('数据删除失败!')
封装的文件处理函数public.py
,也是多对多外键attachments字段数据库处理的方式
def handle_files(request, files_obj, obj, FileSerializer):
"""
该函数传参对象有attachments属性时才可调用
"""
files = []
for file_obj in files_obj:
filename = file_obj.name
suffix = filename.rsplit(".", 1)[1]
file_data = {
'file': file_obj,
'filename': filename,
'suffix': suffix,
}
if request.method == 'PUT':
# 1.本地文件删除
obj_files = obj.attachments.all()
for file in obj_files:
file.file.delete()
# 2.文件数据记录删除(先删除子表数据记录)
obj_files.delete()
fs = FileSerializer(data=file_data)
if fs.is_valid():
new_file = fs.save()
new_file.content_object = obj
new_file.save()
files.append(fs.data.get('id'))
if request.method == 'POST':
obj.attachments.add(*files)
if request.method == 'PUT':
obj.attachments.set(files)
def delete_data(obj):
"""
该函数传参对象有attachments属性时才可调用
"""
# 设置事务,保证数据的同步
with transaction.atomic():
save_point = transaction.savepoint()
try:
# 1.本地文件删除
files = obj.attachments.all()
if files:
for file in files:
file.file.delete()
# 2.文件数据记录删除(先删除子表数据记录)
files.delete()
# 3.数据删除(再父表数据记录删除)
obj.delete()
except Exception as e:
transaction.rollback(save_point)
logger.info('obj->{} file delete error:{}'.format(obj, e))
else:
transaction.savepoint_commit(save_point)
msg = '数据删除成功!Data loading......'
return msg
def get_page_all_data(total, rows, sortOrder, pageNumber, pageSize):
for i in range(len(rows)):
if sortOrder == 'desc':
no = total - (pageNumber - 1) * pageSize - i
else:
no = (pageNumber - 1) * pageSize + i + 1
rows[i]['no'] = no
data = {"total": total, "rows": rows}
return data
3.分页查询数据处理
class DataStructureListView(APIView):
def get(self, request):
pageSize = int(request.GET.get('pageSize', 10))
pageNumber = int(request.GET.get('pageNumber', 1))
search_kw = request.GET.get('search_kw', '')
sortName = request.GET.get('sortName', '')
sortOrder = request.GET.get('sortOrder', '')
ds_all = DataStructure.objects.all()
# 查询
if search_kw:
ds_all = ds_all.filter(
Q(title__icontains=search_kw) | Q(description__icontains=search_kw) | Q(full_name__icontains=search_kw))
if sortName == 'no':
sortName = 'id'
# 排序
if sortOrder == 'desc':
ds_list = ds_all.order_by('-{}'.format(sortName))[(pageNumber - 1) * pageSize:(pageNumber) * pageSize]
else:
ds_list = ds_all.order_by(sortName)[(pageNumber - 1) * pageSize:(pageNumber) * pageSize]
total = ds_all.count()
# print(ds_list)
# page_data = self.get_page_data(ds_list, total, sortOrder, pageNumber, pageSize)
rows = TeamDataStructureQuerySerializer(ds_list, many=True).data
page_data = get_page_all_data(total, rows, sortOrder, pageNumber, pageSize)
if page_data:
return JsonResponse(page_data)
return api_bad_request('数据加载失败!')
def get_page_data(self, obj_list, total, sortOrder, pageNumber, pageSize):
"""不使用序列化器,前端需要修改attachment字段获取方式"""
ds_list_len = len(obj_list)
rows = []
data = {"total": total, "rows": rows}
for i in range(ds_list_len):
attachments = {}
attachments['file_name'] = [i.filename for i in obj_list[i].attachments.all()]
attachments['id'] = [i.id for i in obj_list[i].attachments.all()]
if sortOrder == 'desc':
no = total - (pageNumber - 1) * pageSize - i
else:
no = (pageNumber - 1) * pageSize + i + 1
row = {
'id': obj_list[i].id,
'no': no,
'title': obj_list[i].title,
'description': obj_list[i].description,
'team': obj_list[i].team,
'created_date': obj_list[i].created_date,
'full_name': obj_list[i].full_name,
'knowledge_category': obj_list[i].knowledge_category,
'attachments': attachments,
}
rows.append(row)
return data
下面附上前端分页的代码
{% extends "base.html" %} {% load static %}
{% block main %}
<style>
.table th, .table td, .table b {
text-align: center;
vertical-align: middle !important;
word-break: break-all;
}
</style>
<section class="section" id="da_kf">
<div class="container">
<div class="row justify-content-center">
<div class="col-12 text-center">
<div class="section-title mb-2">
<h4 class="title mb-4">Key Features</h4>
<p class="text-muted para-desc mx-auto mb-0">Add some introduction here!</p>
</div>
</div><!--end col-->
</div><!--end row-->
<div class="row">
<div class="col-lg-4 col-md-4 mt-4 pt-2">
<div class="card features fea-primary rounded p-4 bg-light text-center position-relative overflow-hidden border-0">
<span class="h1 icon2 text-primary">
<a href="{% url 'stdfiles:download' 'HighRise Parameters & Values List' %}" class="text-reset">
{# <img src="{% static 'images/homepage/file-publisher.svg' %}" class="avatar avatar-md-sm"#}
{# alt="">#}
<i class="fas fa-stream"></i>
</a>
</span>
<div class="card-body p-0 content">
<h6><a href="{% url 'stdfiles:download' 'HighRise Parameters & Values List' %}"
class="text-reset">Highrise Parameter
List</a></h6>
</div>
<span class="big-icon text-center">
<i class="fas fa-stream"></i>
</span>
</div>
</div><!--end col-->
<div class="col-lg-4 col-md-4 mt-4 pt-2">
<div class="card features fea-primary rounded p-4 bg-light text-center position-relative overflow-hidden border-0">
<span class="h1 icon2 text-primary">
<a href="{% url 'registers:comingsoon' %}" class="text-reset">
<i class="fas fa-braille"></i>
</a>
</span>
<div class="card-body p-0 content">
<h6><a href="{% url 'registers:comingsoon' %}" class="text-reset">Common Platform Parameter
List</a></h6>
</div>
<span class="big-icon text-center">
<i class="fas fa-braille"></i>
</span>
</div>
</div><!--end col-->
<div class="col-lg-4 col-md-4 mt-4 pt-2">
<div class="card features fea-primary rounded p-4 bg-light text-center position-relative overflow-hidden border-0">
<span class="h1 icon2 text-primary">
<a href="{% url 'registers:comingsoon' %}" class="text-reset">
<i class="fas fa-cogs"></i>
</a>
</span>
<div class="card-body p-0 content">
<h6><a href="{% url 'registers:comingsoon' %}" class="text-reset">Other Features</a></h6>
</div>
<span class="big-icon text-center">
<i class="fas fa-cogs"></i>
</span>
</div>
</div><!--end col-->
</div>
</div><!--end container-->
</section><!--end section-->
<!-- Key feature End -->
<!-- data structure index start -->
<section class="section bg-light">
<div class="container">
<div class="row justify-content-center">
<div class="col-12 text-center">
<div class="section-title mb-4 pb-2">
<h4 class="title mb-4">Data Structure List</h4>
</div>
</div><!--end col-->
</div><!--end row-->
<div id="toolbar">
<div class="form-inline" role="form">
<button id="remove" class="btn btn-soft-primary btn-sm"
onclick="window.open('/teams/add_data_structure/')">
<i class="fa fa-plus"></i> Add new data structure
</button>
<!-- 自定义搜索查询 -->
<div class="input-group" style="left: 5px"><input id="search-keyword"
class="form-control search-input" type="search"
placeholder="Search" autocomplete="off">
<div class="input-group-append">
<button id="search-button" class="btn btn-soft-primary btn-sm" type="button" name="search"
title="Search">
<i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
<!-- data structure index pagination -->
<table id="table"
class="table-sm small"
data-pagination="true"
data-buttons-class="soft-primary btn-sm"
data-sort-name="created_date"
data-sort-order="desc"
data-remember-order="true"
data-show-fullscreen="true"
data-show-columns="true"
data-show-columns-toggle-all="true"
data-show-export="true"
data-click-to-select="true"
data-toolbar="#toolbar"
style="table-layout: fixed;height: auto"
>
<thead class="thead-light">
</table>
</div>
</section>
<!-- data structure index end -->
<!-- Team Members Start -->
<section class="section" id="da_team">
<!-- Companies Start -->
<div class="container">
<div class="row justify-content-center">
<div class="col-12 text-center">
<div class="section-title mb-2">
<h4 class="title mb-4">Team Members</h4>
<p class="text-muted para-desc mx-auto mb-0">Add some introduction here!</p>
</div>
</div><!--end col-->
</div><!--end row-->
<div class="row">
<div class="col-lg-4 col-md-6 col-12 mt-4 pt-2">
<a href="page-job-detail.html">
<div class="media key-feature align-items-center p-3 rounded shadow">
<i class="fas fa-user-check"></i>
<div class="media-body ml-3">
<h4 class="title mb-0 text-dark">Tian Xia</h4>
<p class="text-muted mb-0">Manager,....</p>
</div>
</div>
</a>
</div><!--end col-->
<div class="col-lg-4 col-md-6 col-12 mt-4 pt-2">
<a href="page-job-detail.html">
<div class="media key-feature align-items-center p-3 rounded shadow">
<i class="fas fa-user-check"></i>
<div class="media-body ml-3">
<h4 class="title mb-0 text-dark">Yang Fang</h4>
<p class="text-muted mb-0">Responsibility</p>
</div>
</div>
</a>
</div><!--end col-->
<div class="col-lg-4 col-md-6 col-12 mt-4 pt-2">
<a href="page-job-detail.html">
<div class="media key-feature align-items-center p-3 rounded shadow">
<i class="fas fa-user-check"></i>
<div class="media-body ml-3">
<h4 class="title mb-0 text-dark">Wang Xiaonan</h4>
<p class="text-muted mb-0">Responsibility</p>
</div>
</div>
</a>
</div><!--end col-->
<div class="col-lg-4 col-md-6 col-12 mt-4 pt-2">
<a href="page-job-detail.html">
<div class="media key-feature align-items-center p-3 rounded shadow">
<i class="fas fa-user-check"></i>
<div class="media-body ml-3">
<h4 class="title mb-0 text-dark">Ding Chenchen</h4>
<p class="text-muted mb-0">Responsibility</p>
</div>
</div>
</a>
</div><!--end col-->
</div><!--end row-->
</div><!--end container-->
<!-- Companies End -->
</section>
<!-- Team Members End -->
{% endblock %}
{% block script %}
<!-- JS for download table-->
<script>
var $table = $('#table')
$(function () {
$('#toolbar').find('select').change(function () {
$table.bootstrapTable('destroy').bootstrapTable({
exportDataType: $(this).val(),
exportTypes: ['excel', 'xml', 'csv', 'txt', 'sql'],
columns: [
{
field: 'state',
checkbox: true,
visible: $(this).val() === 'selected'
}
]
})
}).trigger('change')
})
</script>
<!-- JS for pagination -->
<script>
var $articlesTable = $('#table').bootstrapTable('destroy').bootstrapTable({
url: '/teams/data_structure_list/',
method: 'GET',
dataType: "json",
uniqueId: 'id', //每一行的唯一标识,一般为主键列
striped: false, //是否显示行间隔色
cache: false,
sortName: 'no',
sortable: true,
sortOrder: 'desc',
sidePagination: "server",
undefinedText: '--',
singleSelect: true,
toolbar: '#toolbar', //工具按钮用哪个容器
showToggle: true, //是否显示详细视图和列表视图的切换按钮
strictSearch: true,
clickToSelect: true,
pagination: true, //是否显示分页(*)
showRefresh: true, //是否显示刷新按钮
pageNumber: 1, //初始化加载第一页,默认第一页
pageSize: 10, //每页的记录行数(*)
pageList: [10, 20, 50, 100, 'all'],
paginationPreText: "<",
paginationNextText: ">",
queryParamsType: "",
maxHeight: "200px",
queryParams: function (params) {
var query_params = {
'pageSize': params.pageSize,
'pageNumber': params.pageNumber,
'search_kw': $('#search-keyword').val(), // 查询框中的参数传递给后台
'sortName': params.sortName,
'sortOrder': params.sortOrder
};
return query_params;
},
columns: [
{
field: 'no',
title: 'No',
align: 'center',
halign: 'center',
width: '60px',
visible: true,
sortable: true,
formatter: function (value, row, index) {
var result = "";
result += '<a href="/teams/data_structure_detail/' + row.id + '/">' + row.no + '</a>'
return result
}
},
{
field: 'id',
title: 'DB_Id',
align: 'center',
halign: 'center',
width: '70px',
visible: false,
sortable: true,
formatter: function (value, row, index) {
{#console.log(row)#}
var result = "";
result += '<a href="/teams/data_structure_detail/' + row.id + '/">' + row.id + '</a>'
return result
}
},
{
field: 'title',
title: 'title',
align: 'left',
halign: 'center',
width: '180px',
visible: true,
formatter: function (value, row, index) {
var result = "";
result += '<a href="/teams/data_structure_detail/' + row.id + '/">' + row.title + '</a>'
return result
}
},
{
field: 'description',
title: 'Description',
align: 'left',
halign: 'center',
width: '180px',
visible: true,
},
{
field: 'team',
title: 'Team',
align: 'left',
halign: 'center',
width: '100px',
visible: true,
},
{
field: 'knowledge_category',
title: 'Knowledge Category',
align: 'left',
halign: 'center',
width: '170px',
visible: true,
},
{
field: 'full_name',
title: 'Owner',
align: 'left',
halign: 'center',
width: '120px',
visible: true,
sortable: true,
},
{
field: 'created_date',
title: 'Upload date',
align: 'center',
halign: 'center',
width: '120px',
visible: true,
sortable: true,
},
{
field: 'attachments',
title: 'Files',
align: 'left',
halign: 'center',
width: '180px',
visible: true,
formatter: function (value, row, index) {
var result = "";
// 后台不使用序列化器
{#for (var i = 0; i < row.attachments.id.length; i++) {result += '<a href="/file/teams_data_structure_file_download/' + row.attachments.id[i] + '">' + row.attachments.file_name[i] + ';<br/></a>'}#}
// 后台使用序列化器
for (var i = 0; i < row.attachments.length; i++) {
result += '<a href="/file/teams_data_structure_file_download/' + row.attachments[i].id + '">' + row.attachments[i].file_name + ';<br/></a>'
}
return result
}
},
{
title: 'Operation',
halign: 'center',
width: '80px',
visible: true,
formatter: function (value, row, index) {
var result = "";
result += '<a href="/teams/data_structure_detail/' + row.id + '/" target="_blank"><i class="fas fa-edit"></i></a> '
result += '<a href="#" οnclick="del_data_structure(' + row.id + ')"><i class="fas fa-trash-alt"></i></a>'
return result
}
}
],
onLoadError: function (data) {
console.log("数据加载失败!", "错误提示");
$.messager.alert({title: '提示', msg: '数据加载失败!', icon: 'warning', top: 200});
},
});
// 搜索查询按钮触发事件
$("#search-button").click(function () {
console.log($('#search-keyword').val())
$('#table').bootstrapTable(('refresh'));
})
// 回车执行搜索
$("#search-keyword").bind('keyup', function (event) {
console.log($('#search-keyword').val())
$('#table').bootstrapTable(('refresh'));
})
</script>
<!-- JS for delete data -->
<script>
function del_data_structure(id) {
console.log(id)
$.messager.confirm({
title: '提示', msg: '是否确认删除该条数据?', top: 200, fn: function (r) {
if (r) {
$.ajax({
url: server_url + '/teams/data_structure_detail/' + id + '/',
method: 'delete',
processData: false,
contentType: false,
cache: false,
success: function (data) {
console.log("data:" + data);
console.log("data:" + data.status);
if (data.status === 200) {
{#$.messager.alert({title: '提示', msg: data.msg, icon: 'warning', top: 200,});#}
$.messager.show({
title: '提示',
msg: data.msg,
showType: '',
timeout: 500,
style: {top: 200}
});
console.log("data:" + data.msg);
$('#table').bootstrapTable('refresh');
{#window.setTimeout("window.location=server_url+'/teams/data_structure/'", 600);#}
return
}
console.log(data)
$.messager.alert({title: '提示', msg: '权限不足或服务请求异常,数据无法删除!', icon: 'warning', top: 200});
},
//请求失败,包含具体的错误信息
error: function (data) {
console.log('error' + data.msg);
$.messager.alert({title: '提示', msg: '请求服务错误或当前网络不佳!', icon: 'warning', top: 200});
}
});
}
}
});
}
</script>
{% endblock %}
免声明:以上内容和代码仅供参考!