Django 项目总结

Django模型类

1. 继承Django用户模型类
class User(AbstractUser):
    """用户模型类"""
    mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')

    class Meta:
        db_table = 'tb_users'
        verbose_name = '用户'
        verbose_name_plural = verbose_name  # 去掉s
2. 配置
AUTH_USER_MODEL = 'users.User'

AUTH_USER_MODEL 参数的设置以点.来分隔,表示应用名.模型类名

注意:Django建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现未知错误。

3. 执行数据库迁移
python manage.py makemigrations
python manage.py migrate
4. 管理器方法(创建用户密码加密)

管理器方法即可以通过User.objects. 进行调用的方法。

  • create_user(username, email=None, password=None, *extra_fields) # 会自动给用户的密码进行加密*

    创建、保存并返回一个User对象。

  • create_superuser(username, email, password, **extra_fields)

    create_user() 相同,但是设置is_staffis_superuserTrue

5. 常用方法(检查密码)
  • set_password(raw_password)

    设置用户的密码为给定的原始字符串,并负责密码的。 不会保存User 对象。当Noneraw_password 时,密码将设置为一个不可用的密码。

  • check_password(raw_password) # 检验密码是否正确

    如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。

6. 创建自定义的用户模型类
# 自关联
class Area(models.Model):
    name = models.CharField(verbose_name="名称", max_length=20)
    parent = models.ForeignKey("self", on_delete=models.SET_NULL, related_name="subs", verbose_name="上级行政区", null=True,blank=True)  
    # 自关联,外键属性设置在多方,on_delete表示是否在删除一方时。多方的数据是否也删除
    # realted_name = "subs" ,表示反属性,等于 多类小写_set
    # 这个self表示自关联,一般为类名,有的时候还会是users.User,表示应用名.类名 
    class Meta:
        db_table = 'tb_users'  # 表名
        verbose_name = '用户'  # 显示用户表的表名
        verbose_name_plural = verbose_name  # 去掉s

序列化器

1. 创建序列化器
from rest_framework import serializers
# 第一种方法:
class UserAddBrowseHistorySerializer(serializers.Serializer):
    """历史浏览添加序列化器"""
    sku_id = serializers.IntegerField(label='商品SKU编号', min_value=1)

# 第二种方法
class UserAddressSerializer(serializers.ModelSerializer):
    """
    用户地址序列化器
    """
    province = serializers.StringRelatedField(read_only=True)
    city = serializers.StringRelatedField(read_only=True)
    district = serializers.StringRelatedField(read_only=True)
    province_id = serializers.IntegerField(label='省ID', required=True)
    city_id = serializers.IntegerField(label='市ID', required=True)
    district_id = serializers.IntegerField(label='区ID', required=True)

    class Meta:
        model = Address  # 指定模型类
        exclude = ('user', 'is_deleted', 'create_time', 'update_time')  # 去掉这些字段
        # fields = ("id","user",...)  序列化这些字段
        fields = "__all__"  # 表示所有
        # extra_kwargs = { "字段":{ "选项名":值}  }  
        # extra_kwagrs = {"user":{"read_only":True}}
        # read_only_fields = ('id', 'bread', 'bcomment') 这些字段都设置成read_only = True
# 注意:如果数据只用来序列化或反序列化,就不需要去考虑,在里面添加read_only(序列化,json->obj)和write_only(反序列化,obj-json)
2. 序列化设置关联属性(在单方里)
# 第一种 多方模型类小写_set
# PrimaryKeyRelatedField,例如,关联属性名 要写成 多方模型类小写_set
heroinfo_set = serializers.PrimaryKeyRelatedField(label="英雄", many=True, read_only=True)
# 得到的都是英雄对应的id,例如:"heroinfo_set": [1,2,3,4,5]

# 第二种
heroinfo_set = HeroInfoSerializre(many=True)  # 该类为多方的序列化器类
# 得到的是英雄序列化器类的序列化的信息,需要添加 many = True

# 第三种
# (__str__方法的返回值)单方模型类里的
heroinfo_set = serializers.StringRelatedField(label="英雄", many=True)
"heroinfo_set": ["郭靖","黄蓉","黄药师","欧阳锋","梅超风"]
3. 序列化设置关联属性(在多方里)
# 第一种
# PrimaryKeyRelatedField,此字段将被序列化为关联对象的主键。得到关联属性的id,得到的"hbook":1
hbook = serializers.PrimaryKeyRelatedField(label='图书', read_only=True)
或
hbook = serializers.PrimaryKeyRelatedField(label='图书', queryset=BookInfo.objects.all())

# 第二种
# 使用关联对象的序列化器,得到的是图书序列化器类的序列化的信息
hbook = BookInfoSerializer() # 该类为单方的序列化器类
# 得到的"hbook"为:
"hbook": { 
  "id": 1,
  "btitle": "\u5c04\u96d5\u82f1\u96c4\u4f20",
  "bpub_date": "1980-05-01",
  "bread": 12,
  "bcomment": 34,
  "image": null
 }
    
 # 第三种
3) StringRelatedField
# 此字段将被序列化为关联对象的字符串表示方式(即__str__方法的返回值)
hbook = serializers.StringRelatedField(label="书籍")
# 模型类
"hbook": "射雕英雄传"
4. 序列化校验字段
# 第一种
# validators,在字段中添加validators选项参数,也可以补充验证行为
btitle = serializers.CharField(label='标题', max_length=20, validators=[check_Django])  
# 函数名,在上面定义函数 def check_Django().....

# 第二种
# validate_,指定函数名,例如,在图书序列器类里定义一个函数,
class BookInfoSerializer(serializers.Serializer):
    """图书数据序列化器"""
	pass
    def validate_btitle(self, value): # value为前端传过来的数据,即为bittle的数据
	pass

# 第三种
# validate,例如,在序列化器中需要同时对多个字段进行比较验证时,可以定义validate方法来验证
class BookInfoSerializer(serializers.Serializer):
    """图书数据序列化器"""
	pass
    def validate(self, attrs):  # attrs接收传过来的字典数据,即为request.data
	pass

# 当条件不满足时抛出异常:raise serializers.ValidationError("图书不是django")
5. 序列化器在类视图里的使用
5.1 序列化使用
# 1.先查询出一个图书对象
book = BookInfo.objects.get(id=2)
# 2.构造序列化器对象
serializer = BookInfoSerializer(book)
# 3.获取序列化数据
return Response(serializer.data)
# 注意: many = True
# 如果要被序列化的是包含多条数据的查询集QuerySet,可以通过添加many=True参数补充说明,序列化类里面添加 
book_qs = BookInfo.objects.all()
serializer = BookInfoSerializer(book_qs, many=True)
serializer.data
5.2 反序列化使用
 def post(self, request):
        """新建一本图书信息"""
        # 1.获取参数
        # 前端传递参数时,使用json传递
        # 获取参数(btitle, bpub_date)并进行参数校验
        req_data = request.body.decode()  # bytes,经过decode变成字符串
        # 将json字符串-> python字典
        req_dict = json.loads(req_data)

        # 2.创建反序列化对象
        serializer = BookInfoSerializer(data=req_dict, many=True)
        # 3.参数校验
        serializer.is_valid(raise_exception=True)
        # 4.反序列化-数据保存
        serializer.save()
        # 5.返回json数据
        return JsonResponse(serializer.data, status=201)
6. 反序列化-数据保存 serializer.save()
6.1 调用create()方法
# 调用create()方法 增加,只传递数据 
serializer = BookInfoSerializer(data=data)  # 没有传对象
serializer.save()  # create()  该序列器类 BookInfoSerializer里的方法
# 该方法定义在序列化器类里
	def create(self, validated_data):  # 接受校验后的数据,即为字典,所以只需要把字典解包就可以了
        # print("调用了create方法")
        # 创建图书对象
        book = BookInfo.objects.create(**validated_data)  # 解包
        return book
6.2 调用update()方法
# 调用update()方法, 更新,需要传递对象和数据 
# 获取id为1的图书对象
book = BookInfo.objects.get(id=1)
# 创建图书序列化器类对象
serializer = BookInfoSerializer(book, data=data)
serializer.save()  # update() 该序列器类 BookInfoSerializer里的方法

def update(self, instance, validated_data):
    # print("调用了update方法")
    # 更新对象,instance为图书的对象
    instance.bpub_date = validated_data.get("bpub_date")
    instance.btitle = validated_data.get("btitle")
    # 模型类里的save(),添加和更新都要这个操作,这个是用对象来操作添加和更新
    instance.save()
    return instance  # 返回图书对象
6.3 说明
"""
1) 在对序列化器进行save()保存时,可以额外传递数据,这些数据可以在create()和update()中的validated_data参数获取到

2)默认序列化器必须传递所有required的字段,否则会抛出验证异常。但是我们可以使用partial参数来允许部分字段更新,有的字段就可以不用传
# Update `comment` with partial data
serializer = BookInfoSerializer(book, data=data, partial=True)  # 部分更新
"""
7.获取序列化数据
# 1. 接收参数selected并进行校验(必传)
serializer = CartSelectAllSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

# 获取参数,序列化后的数据
selected = serializer.validated_data['selected']

# 2. 获取user并进行处理
try:
    # user是被@property装饰的函数
    user = request.user  
    except Exception as e:
        user = None

redis

1. redis的存取

redis的优点:从redis中存取数据,效率要比mysql数据库要快的多

redis里面存取的数据类型:

# 注意:从redis里取出的数据都为bytes类型,需要对其进行编译decode()
# string: 字符串  
	'history_': <value>	
	history_1: '1,2,3'
	history_2: 

# hash: 哈希 value: 属性:值    key: : 
	history: {
        history_<user_id>: <value>,
	}

# list: 列表  key:[,]
	'history_': [1, 2, 3]
	
# set: 无序集合  key:(,)
	排除

# zset: 有序集合
	有序集合中的每个元素都有一个权重。
2. redis管道操作
# 注意:查询redis里的数据的时候不用管道,修改redis里数据的时候用管道
def create(self, validated_data):
        """在redis中保存用户浏览的记录"""
        # 获取登录用户user
        user = self.context['request'].user 
        # context为序列化器类对象里的一个属性,而且是一个字典

        # 获取redis链接  history为配置里redis存储的位置
        redis_conn = get_redis_connection('history') # type:StrictRedis
        history_key = 'history_%s' % user.id

        # 创建redis管道对象  
        pl = redis_conn.pipeline()

        # 去重:如果用户已经浏览过该商品,先将商品id从列表中移除。
        sku_id = validated_data['sku_id']
        # lrem(key, count, value): 如果value存在直接移除,不存在就忽略
        pl.lrem(history_key, 0, sku_id)  # 操作redis里的方法

        # 左侧加入: 保持浏览顺序
        # lpush(key, *values): 向列表左侧加入元素
        pl.lpush(history_key, sku_id)

        # 截取: 只保留最近几条浏览记录。
        # ltrim(key, start, end): 截取列表元素,只保留指定区间内的元素
        pl.ltrim(history_key, 0, constants.USER_BROWSING_HISTORY_COUNTS_LIMIT-1)

        # 执行管道命令
        pl.execute()

        # 返回
        return validated_data
3.redis操作命令

注意:从redis里取出的值都为bytes类型

hash命令: 格式: key : { }

1)hincrby(key, field, value)

# 给hash中field属性值累加一个value,如果field不存在,新建一个属性和值,例如
# 如果用户的购物车记录中已经添加过该商品,数量需要进行累加
# 如果用户的购物车记录中没有添加过该商品,设置一个新的属性和值
# hincrby(key, field, value): 给hash中field属性值累加一个value,如果field不存在,新建一个属性和值
redis_conn.hincrby(cart_key, sku_id, count)  # sku_id商品的id,count商品的数量

2)hgetall(key)

# 返回hash中所有属性和值
cart_redis = redis_conn.hgetall(cart_key)  # dict

3)hset(key, field, value)

 # hset(key, field, value): 将hash中指定属性field的值设置为value
 pl.hset(cart_key, sku_id, count)

4)hdel(key, *fields)

# hdel(key, *fields): 删除hash中对应的属性和值,有则删除,无则忽略
pl.hdel(cart_key, sku_id)

5)hkeys(key)

 # hkeys(key): 返回hash中所有属性
 # [b'', b'', ...]
 sku_ids = redis_conn.hkeys(cart_key)  # list

set命令:

1)sadd(key, *members)

# 向set中添加元素,不需要关注是否重复,set中元素唯一
# sadd(key, *values): 向set中添加元素,不需要关注是否重复,set中元素唯一
redis-conn.sadd(cart_selected_key, sku_id)  # sku_id商品的id

2)smembers(key)

# 获取set中所有元素
 sku_ids = redis_conn.smembers(cart_selected_key)

3)srem(key, *members)

# srem(key, *members): 从set中移除元素,如果元素不存在,直接忽略
pl.srem(cart_selected_key, sku_id)

list命令:

1)lrange(key, start, end )

# lrange(key, start, end): 获取redis列表指定区间内的元素
# [b'', b'', ...]
sku_ids = redis_conn.lrange(history_key, 0, 4)
4.两个Redis指令参考链接
  • Redis原生指令参考 http://redisdoc.com/index.html
  • Redis python客户端 方法参考 http://redis-py.readthedocs.io/en/latest/#indices-and-tables

视图(DRF框架)

1. APIView类
# 需要定义请求方式get,post,delete,put
# 返回Response
2.获取参数
# request.data
包含客户端发送请求体数据(表单数据或json数据),原始Django request对象通过request.POST和request.body,
并且已经转成的字典(dict, OrderedDict, QueryDict)

# request.query_params
包含查询字符串的数据,相当原始Django request对象request.GET并且已经转成的字典(dict, OrderedDict, QueryDict)

# self.kwargs
保存的是从url地址中提取的所有命名参数,为一个字典
例如,127.0.0.1:8000/goods/66,
应用里的url("^goods/(?P\d+)$", views.GoodsView.as_view())
得到pk的值:self.kwargs.get("pk")

# self.action
def get_serializer_class(self)
	if self.action == "list":
        return UserSerializer  # 返回不同的序列化器类
    else:
        return BookSerializer

# request.user  登录用户 ,前端请求头里传JWT token数据才能这样用
def get_object(self):
        """返回当前登录user"""
        return self.request.user  # request 为self.request

    # def get(self, request):
         """
         self.request: request对象
         request.user:
             1. 如果用户已认证,request.user就是登录的用户对象
             2. 如果用户未认证,request.user就是匿名用户的对象
3. GenericAPIView类
# 里面封装了四大方法: get_serializer()
# 指明序列化器类
serializer_class = CreateUserSerializer
# 指定查询集(不用的时候,可以不用指定)
queryset

# get_serializer_class(self) 得到序列化器类
返回序列化器类,默认返回serializer_class,可以重写

# get_serializer(self, args, *kwargs) 得到序列化器类对象
返回序列化器对象,主要用来提供给Mixin扩展类使用,如果我们在视图中想要获取序列化器对象,也可以直接调用此方法。

# get_queryset(self)  得到查询集
返回视图使用的查询集,主要用来提供给Mixin扩展类使用,是列表视图与详情视图获取数据的基础,默认返回queryset属性

# get_object(self)  得到对象,默认是根据pk即id查出来的对象
返回详情视图所需的模型类数据对象,主要用来提供给Mixin扩展类使用。
在试图中可以调用该方法获取详情信息的模型类对象。
默认,按照pk(id)来得到对象
4. Mixin五个扩展类
# 里面分别封装了:5个action操作方法 list create retrieve update destroy
# 扩展类一般和GenericViewSet搭配使用,注意:继承的时候,扩展类写在前面,视图集类写在后面

DRF提供了5个扩展类,封装了5个通用的操作流程。

类名 说明
ListModelMixin 提供了一个list方法,封装了返回模型数据列表信息的通用流程。
CreateModelMixin 提供了一个create方法,封装了创建一个模型对象数据信息的通用流程。
RetrieveModelMixin 提供了一个retrieve方法,封装了获取一个模型对象数据信息的通用流程。
UpdateModelMixin 提供了一个update方法,封装了更新一个模型对象数据信息的通用流程。
DestroyModelMixin 提供了一个destroy方法,封装了删除一个模型对象数据信息的通用流程。
5. 子类视图

为了方便我们开发RestAPI,DRF框架除了提供APIView和GenericAPIView视图类之外,还提供了一些子类视图类,这些子类视图类同时继承了GenericAPIView和对应的Mixin扩展类,并且提供了对应的请求方法。

类名 说明
ListAPIView 1)继承自ListModelMixin和GenericAPIView。2)如果想定义一个视图只提供列出模型列表信息的接口,继承此视图类是最快的方式。
CreateAPIView 1)继承自CreateModelMixin和GenericAPIView。2)如果想定义一个视图只提供创建一个模型信息的接口,继承此视图类是最快的方式。
RetrieveAPIView 1)继承自RetrieveModelMixin和GenericAPIView。2)如果想定义一个视图只提供获取一个模型信息的接口,继承此视图类是最快的方式。
UpdateAPIView 1)继承自UpdateModelMixin和GenericAPIView。2)如果只想定义一个视图只提供更新一个模型信息的接口,继承此视图类是最快的方式。
DestroyAPIView 1)继承自DestroyModelMixin和GenericAPIView。2)如果只想定义一个视图只提供删除一个模型信息的接口,继承此视图类是最快的方式。
ListCreateAPIView 1)继承自ListModelMixin,CreateModelMixin和GenericAPIView。2)如果只想定义一个视图提供列出模型列表创建一个模型信息的接口,继承此视图类是最快的方式。
RetrieveUpdateAPIView 1)继承自RetrieveModelMixin,UpdateModelMixin和GenericAPIView。2)如果只想定义一个视图提供获取一个模型信息更新一个模型信息的接口,继承此视图类是最快的方式。
RetrieveDestroyAPIView 1)继承自RetrieveModelMixin,DestroyModelMixin和GenericAPIView。2)如果只想定义一个视图提供获取一个模型信息删除一个模型信息的接口,继承此视图类是最快的方式。
RetrieveUpdateDestoryAPIView 1)继承自RetrieveModelMixin,UpdateModelMixin,DestroyModelMixin和GenericAPIView。2)如果只想定义一个视图提供获取一个模型信息更新一个模型信息删除一个模型信息的接口,继承此视图类是最快的方式
6.视图集

将操作同一资源的处理方法放在同一个类中(视图集),处理方法不要以请求方式命名,而是以对应的action命名,

​ list: 提供一组数据 例如,获取所有图书信息(list) GET /books/

​ create: 创建一条新数据 例如,新建一本图书信息(create) POST /books/

​ retrieve: 获取指定的数据 例如,获取指定的图书信息(retrieve) GET /books/(?P\d+)/

​ update: 更新指定的数据 例如,修改指定的图书信息(update) PUT /books/(?P\d+)/

​ destroy: 删除指定的数据 例如,删除指定的图书信息(destroy) DELETE /books/(?P\d+)/

进行url配置时需要指明请求方法和处理函数之间的对应关系。

类名 说明
ViewSet 1)继承自ViewSetMixin和APIView。2)如果使用视图集时不涉及数据库和序列化器的操作,可以直接继承此类。
GenericViewSet 1)继承自ViewSetMixin和GenericAPIView。2)如果使用视图集涉及数据库和序列化器的操作,可以直接继承此类。
ModelViewSet 1)继承自5个Mixin扩展类和GenericViewSet。2)如果使用视图集想一次提供通用的5种操作,继承这个类是最快的。
ReadOnlyModelViewSet 1)继承自ListModelMixin,RetrieveModelMixin和GenericViewSet。2)如果使用视图集想一次提供list操作和retrieve操作,继承这个类是最快的。
7.路由route的配置
1)路由Router是专门配合视图集来使用的,可以使用Router自动生成视图集中相应处理函数对应的URL配置项。

2)使用Router自动生成视图集中相应处理函数对应的URL配置项时,除了常见的5种基本操作之外,如果视图集中有添加的其他处理方法,则需要给这些方法加上action装饰器之后,才会动态生成其对应的URL配置项。
# 应用urls.py:
urlpatterns = [
    url('^index/$', views.index),
]
route = DefaultRouter()
route.register(r'books', BookInfoViewSet, base_name='books')  # books为前缀不是正则表达式
urlpatterns += route.urls
"""
register里的参数:
prefix 该视图集的路由前缀
viewset 视图集
base_name 路由名称的前缀
"""
# @action的作用,对于视图集
在视图集中,如果想要让Router自动帮助我们为自定义的动作生成路由信息,需要使用rest_framework.decorators.action装饰器。@action(method=['GET'],detail=True)
"""
methods: 声明该action对应的请求方式,列表传递
detail: 声明该action的路径是否与单一资源对应,及是否是xxx//action方法名/
True 表示路径格式是xxx//action方法名/ ,例如,books/6/read
False 表示路径格式是xxx/action方法名/,例如,books/latest/
"""

Django其它

1.认证 authentication
# setting.py 里设置,全局认证都要满足该认证
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',   # 基本认证
        'rest_framework.authentication.SessionAuthentication',  # session认证
    )
}
# views.py 每个视图中通过设置authentication_classess属性来设置
class BookInfoViewSet(ModelViewSet):
    authentication_classes = (SessionAuthentication, BasicAuthentication)
2.权限Permissions
# setting.py 里设置,全局权限都要满足该权限
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}
# views.py 视图集或视图函数
class BookInfoViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated]
    
# 自定义权限
3.限流Throttling
4.过滤Filtering和排序
INSTALLED_APPS = [
    'django_filters',  # 需要注册应用,
]
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}
# 指定过滤字段
# 127.0.0.1:8000/books/?bread=20 访问的地址
# 123.0.0.1:8000/books/?btitle=西游记
filter_fields = ("btitle","bread")

# 指定排序字段
# 127.0.0.1:8000/books/?ordering=-bread,降序
filter_backends=[OrderingFilter]
ordering_fields = ("id","bpub_date","bread")
5.分页Pagination
# setting.py:
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS':  'rest_framework.pagination.PageNumberPagination',  # 全局
    'PAGE_SIZE': 2  # 每页数目,显示2条
}
# views.py:
# 127.0.0.1:8000/books/?offset=3&limit=2
# pagination_class = LimitOffsetPagination  
# 127.0.0.1:8000/books 默认用全局的
pagination_class = PageNumberPagination
# 关闭当前视图分页
pagination_class = None

# 自定义分页
class StandrdResultPagination(PageNumberPagination):
    # 127.0.0.1/books/?page=1&page_size=2
    # 指定默认页容量
    page_size = 3
    # 指定获取分页数据是页容量参数名字
    page_size_query_param = 'page_size'
    # 指定最大页容量
    max_page_size = 5
# 使用LimitOffsetPagination分页时,获取分页数据时可以传递参数offset(偏移量)和limit(限制条数)。
6.异常处理 Exceptions

拓展

1.数据的转换(字典)

json

​ json.dumps(dict): 将python字典转换成json字符串。

​ json.loads(json_str): 将json字符串转换成python字典。

pickle:

​ pickle.dumps(obj): 将obj转换成bytes字节流

​ pickle.loads(bytes字节流):将bytes字节流转换成obj

base64:

​ base64.b64encode(byte字节流): 将byte字节流进行base64编码,返回编码之后内容(bytes字节流)

​ base64.b64decode(bytes字节流&str): 将参数内容进行base64解码,返回解码之后内容(bytes字节流)

设置购物车cookie数据:

​ res = base64.b64encode(pickle.dumps(cart_dict)).decode()

解析cookie购物车数据:

​ res = pickle.loads(base64.b64decode(cart_data))

2.Django里文件单独运行
# 添加运行的配置环境
if not os.getenv('DJANGO_SETTINGS_MODULE'):  
    os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev'  # 运行环境的配置文件件
    
# 让Django进行一次初始化
import django
django.setup()

# 有的还需要添加路径
# 添加搜索包路径
import sys
sys.path.insert(0, '../')
print(sys.path)
3.mysql事务

对于订单保存的过程,其中的数据库操作,要么都应该成功,要么都应该失败。

mysql事务:一组mysql语句,放在同一事务中,要么都成功,要么都失败。

1)mysql事务基本操作:

​ begin或start transaction:手动开启一个mysql事务。

​ commit: 事务提交,让事务中sql语句的执行结果永久有效。

​ rollback: 事务回滚,撤销事务中sql语句的执行结果。

2)mysql事务的保存点:

​ 在mysql事务中,可以设置事务保存点,在进行事务语句回滚时可以只回滚到某个保存点。

​ savepoint 保存点名称: 设置mysql事务保存点。

​ rollback to 保存点名称: 回滚事务语句到保存点的位置,保存点之后sql语句的执行结果会被撤销。

3)django使用事务:

from django.db import transaction

with transaction.atomic():
    # 在with语句块中代码,凡是涉及到数据库操作的代码,都会放到同一个事务中
    
    # 设置一个事务保存点
    sid = transaction.savepoint()
    
    # ...
    
    # 回滚到指定的事务保存点  事务并没有结束,结束事务只有commit和rollback
    trnasaction.savepoint_rollback(sid)
4.互斥锁(多并发)

多个人同时购买同一件商品,可能会产生订单并发问题。

订单并发问题解决:

1)悲观锁:

​ 在事务中查询数据的时候尝试对数据进行加锁(互斥锁), 获取到锁的事务可以对数据进行操作,获取不到 锁的事务会阻塞,直到锁被释放。

# select * from tb_sku where id= for update; 设置悲观锁
sku = SKU.objects.select_for_update().get(id=sku_id)

2)乐观’锁’:

​ 本质不是锁,乐观锁本质上不是加锁,查询数据的时候不加锁,对数据进行修改的时候需要进行判断,修改失败需要重新进行尝试。

def create(self, validated_data):
        """保存订单信息(订单并发-乐观锁)"""
        # 获取参数address和pay_method
        address = validated_data['address']
        pay_method = validated_data['pay_method']

        # 组织参数
        # 用户user
        user = self.context['request'].user  # 序列化器类对象里面有context属性

        # 订单id: '年月日时分秒'+用户id
        order_id = datetime.now().strftime('%Y%m%d%H%M%S') + ('%010d' % user.id)

        # 运费
        freight = Decimal(10)  # 精度高

        # 订单商品总数量和实付款
        total_count = 0
        total_amount = Decimal(0)

        # 订单状态
        # if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH']:
        #     # 货到付款
        #     status = OrderInfo.ORDER_STATUS_ENUM['UNSEND'] # 待发货
        # else:
        #     # 在线支付
        #     status = OrderInfo.ORDER_STATUS_ENUM['UNPAID'] # 待支付
        # 三步运算
        status = OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH'] else OrderInfo.ORDER_STATUS_ENUM['UNPAID']

        # 获取redis链接
        redis_conn = get_redis_connection('cart')  # cart配置里存redis的地方

        # 从redis中获取用户所要购买的商品sku_id  set
        cart_selected_key = 'cart_selected_%s' % user.id
        # (b'', ...)
        sku_ids = redis_conn.smembers(cart_selected_key)

        # 从redis中获取用户购物车商品的sku_id和对应的数量count  hash
        cart_key = 'cart_%s' % user.id
        # {
        #     b'': b'',
        #     ...
        # }
        cart_dict = redis_conn.hgetall(cart_key)

        with transaction.atomic():
            # 在with语句块中代码,凡是涉及到数据库操作的代码,都会放到同一个事务中

            # 设置一个事务保存点
            sid = transaction.savepoint()

            try:
                # 1)向订单基本信息表添加一条记录
                order = OrderInfo.objects.create(
                    order_id=order_id,
                    user=user,
                    address=address,
                    total_count=total_count,
                    total_amount=total_amount,
                    freight=freight,
                    pay_method=pay_method,
                    status=status
                )

                # 2)订单中包含几个商品,就需要向订单商品表中添加几条记录
                for sku_id in sku_ids:
                    # 获取用户所要购买的该商品的数量count
                    count = cart_dict[sku_id]
                    count = int(count)

                    for i in range(3):
                        # 根据sku_id获取商品信息
                        # select * from tb_sku where id=;
                        sku = SKU.objects.get(id=sku_id)

                        # 判断商品库存是否充足
                        if count > sku.stock:
                            # 回滚到sid事务保存点
                            transaction.savepoint_rollback(sid)
                            raise serializers.ValidationError('商品库存不足')

                        # 记录商品原始库存
                        origin_stock = sku.stock
                        new_stock = origin_stock - count
                        new_sales = sku.sales + count

                        # 模拟订单并发问题
                        # print('user: %d times: %s stock: %s' % (user.id, i, origin_stock))
                        # import time
                        # time.sleep(10)

                        # 减少商品库存,增加销量  乐观锁的体现
                        # update tb_sku
                        # set stock=, sales=
                        # where id= and stock=;
                        # update方法返回的是更新的行数
                        res = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)

                        if res == 0:
                            if i == 2:
                                # 更新了3次,仍然失败,下单失败
                                # 回滚到sid事务保存点
                                transaction.savepoint_rollback(sid)
                                raise serializers.ValidationError('下单失败2')

                            # 更新失败,重新进行尝试
                            continue

                        # 向订单商品表中添加一条记录
                        OrderGoods.objects.create(
                            order=order,
                            sku=sku,
                            count=count,
                            price=sku.price
                        )

                        # 累加计算订单中商品总数量和总金额
                        total_count += count
                        total_amount += sku.price*count

                        # 更新成功,跳出循环
                        break

                # 实付款
                total_amount += freight
                # 更新订单记录中商品的总数量和实付款
                order.total_count = total_count
                order.total_amount = total_amount
                order.save()
            except serializers.ValidationError:
                # 继续向外抛出异常
                raise
            except Exception as e:
                # 回滚sid事务保存点
                transaction.savepoint_rollback(sid)
                raise serializers.ValidationError('下单失败1')

        # 3)清除购物车中对应的记录
        pl = redis_conn.pipeline()
        pl.hdel(cart_key, *sku_ids)
        pl.srem(cart_selected_key, *sku_ids)
        pl.execute()

        # 返回订单对象
        return order

3)celery任务队列。

​ 把订单保存代码放到celery的任务中,只启动一个worker。

mysql事务的隔离级别:

隔离级别 说明
Repeatable read(可重复读) 在一个事务中执行一个查询语句,获取到结果永远和第一次获取的结果相同,即使其他事务把数据进行修改而且已经提交,当前事务仍然获取不到更新之后结果。
Read committed(读取已提交) 其他事务进行数据的修改并且提交之后,当前事务能获取到更新之后的结果。

Repeatable read是mysql事务默认隔离级别。

修改mysql事务的隔离级别:

1)编辑mysql配置文件

​ sudo vi /etc/mysql/mysql.conf.d/mysqld.conf

2)修改配置信息

[mysqld]
 28 #
 29 # * Basic Settings
 30 #
 31 user        = mysql
 32 pid-file    = /var/run/mysqld/mysqld.pid
 33 socket      = /var/run/mysqld/mysqld.sock
 34 port        = 3306
 35 basedir     = /usr
 36 datadir     = /var/lib/mysql
 37 tmpdir      = /tmp
 38 lc-messages-dir = /usr/share/mysql
 39 skip-external-locking
 40 # 将mysql事务的隔离级别设置为读取已提交
 41 transaction-isolation=READ-COMMITTED

3)重启mysql服务

​ sudo service mysql restart

其它

1.原始sql

# 第一种
User.objects.raw(sql)

# 第二种
from django.db import connection
with connection.cursor() as cs:
    sql = """DESC {};""".format(table_name)
    cs.execute(sql)
    fields = cs.fetchall()
    
# 其它种查看文档,聚合等等

2.事务

# 下面的代码都在一个事务里面执行,可以查看文档
from django.db import transaction
try:
    with transaction.atomic():
        # 删掉所有user id的信息
        IntentSlot.objects.filter(intent_id=intent_id).delete()
        # 对Excel文件进行解析
        data_list = ExcelHelper.load_excel(file_obj)
except Exception as e:
    return

3.操作excel

api
class IntentSlotFileApiView(APIView):
    def post(self, request):
        # 获取传过来的文件,必须的数据,slot_name,entity, slot_rule, entity, request_slot,default_value,is_required
        file_obj = request.data.get('filename')
        bot_id = request.data.get('bot_id')
        intent_id = request.query_params.get("intent_id")

        if not all([intent_id, file_obj]):
            return Response({"msg": "Incomplete parameters", "status": "fail"}, status=status.HTTP_400_BAD_REQUEST)

        # 文件名
        name = file_obj.name
        # 传的不是excel就直接返回,不用解析
        if not (name.endswith('.xlsx') or name.endswith('.xls')):
            return Response({"msg": "Upload file is not Excel", "status": "fail"}, status=status.HTTP_400_BAD_REQUEST)

        try:
            # 下面的代码都在一个事务里面执行
            with transaction.atomic():
                # 删掉所有user id的信息
                IntentSlot.objects.filter(intent_id=intent_id).delete()

                # 对Excel文件进行解析
                data_list = ExcelHelper.load_excel(file_obj)

                # 校验上传数据

        except Exception as e:
            return Response({"msg": repr(e), "status": "fail"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        return Response({"msg": "Upload Success", "status": "success"}, status=status.HTTP_200_OK)

    def get(self, request):
        # 获取参数
        intent_id = request.query_params.get("intent_id")
        if not intent_id:
            return Response({"msg": "Incomplete parameters", "status": "fail"}, status=status.HTTP_400_BAD_REQUEST)

        # 下载模板
        curr_time = datetime.date.today()
        sheet_name = 'intent_slot_%s_%s' % (intent_id, curr_time)

        try:
            # 根据intent_id查询数据
            intent_slot_objs = IntentSlot.objects.filter(intent_id=intent_id)
            if intent_slot_objs:
                result_list = IntentSlotSerializer(intent_slot_objs, many=True).data
            else:
                result_list = []

            # 导出excel对象
            wb = ExcelHelper.export_excel("botzero_intent_slot", sheet_name, result_list)
            # 拼接表名
            file_name = "{}.xlsx".format(sheet_name)
            if os.path.exists(file_name):
                os.remove(file_name)

            response = HttpResponse(content_type='application/msexcel', status=status.HTTP_200_OK)
            response['Content-Disposition'] = 'attachment;filename={}'.format(file_name)
            wb.save(response)
            return response

        except Exception as e:
            return Response({'msg': repr(e), "status": "fail"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
excel_utils
# -*- coding: utf-8 -*-
import openpyxl
from django.db import connection
from openpyxl.styles import Alignment, Font

class ExcelHelper(object):
    @staticmethod
    def del_filed_by_table_name(table_name, results_list):
        if table_name == "temp_faq":
            for field_dict in results_list:
                del field_dict["state"]
                del field_dict["gmt_modify"]
                del field_dict["gmt_create"]
                del field_dict["question_id"]
                del field_dict["category_id"]
                del field_dict["user_id"]
                del field_dict["row_number"]

        elif table_name == "faq_word":
            for field_dict in results_list:
                del field_dict["faq_id"]
                del field_dict["id"]

    @staticmethod
    def load_excel(file_obj):
        wb = openpyxl.load_workbook(file_obj, data_only=True)
        sheets = wb.sheetnames
        worksheet = wb[sheets[0]]
        all_list, title_list = [], []

        for k, row in enumerate(worksheet.rows):
            # 获取表的字段
            if k == 0:
                title_list = [cell.value for cell in row]
            # 获取表的每行字段的值
            else:
                row_list = []
                for cell in row:
                    if isinstance(cell.value, bool):
                        value = str(cell.value).upper()
                    else:
                        value = cell.value
                    row_list.append(value)
                if any(row_list):
                    # 把字段名和字段值一一对应起来,生成字典并添加到row_list里
                    all_list.append(dict(zip(title_list, row_list)))
        return all_list

    @staticmethod
    def export_excel(table_name, sheet_name, results_list):
        wb = openpyxl.Workbook()
        w = wb.create_sheet(sheet_name, 0)
        i = 1
        # results_list的从数据库查询出来的数据
        if not results_list:
            with connection.cursor() as cs:
                sql = """DESC {};""".format(table_name)
                cs.execute(sql)
                fields = cs.fetchall()
            field_dict = {}
            for field in fields:
                field_dict[field[0]] = None
            results_list.append(field_dict)

        # 根据表名删除不必要的字段
        ExcelHelper.del_filed_by_table_name(table_name, results_list)

        for k, v in results_list[0].items():
            w.cell(row=1, column=i).value = k  # .replace("t_", "").replace("F_", "")
            s = chr(i + 64) + str(1)
            w[s].alignment = Alignment(horizontal='center', vertical='center')
            w[s].font = Font(bold=True)
            w.column_dimensions[chr(i + 64)].width = 35
            i = i + 1
        excel_row = 2
        # write data
        for i in range(0, len(results_list)):
            excel_col = 1
            for k, v in results_list[i].items():
                w.cell(row=excel_row, column=excel_col).value = v
                s = chr(excel_col + 64) + str(excel_row)
                w[s].alignment = Alignment(horizontal='center', vertical='center')
                excel_col = excel_col + 1
            excel_row = excel_row + 1
        return wb

项目注意点

# 表示用户认证,即用户登录的状态
if user.is_authenticated:
    pass # 用户登录执行

# 未登录用户状态,储存数据,可以存到其浏览器里的cookie里
# cookie里的value值必须为字符串

# 前端请求头里传 JWT token数据,或请求头里有Authorization字段,才能用request.user

# 使用配置里的变量,需要这样导入 from django.conf import settings
settings.变量

单元测试

步骤

# 第一步在api模块下的tests.py文件下编写testCase

tests.py

from django.test import TestCase, Client
# Create your tests here.

class ApiTestCase(TestCase):
    def setUp(self) -> None:
        print("这个方法会在每一个测试方法执行之前被调用,通常用来做一些准备工作")
        # 实例化 client 对象
        self.token = "s"
        self.client = Client()

    def tearDown(self) -> None:
        print("这个方法会在每一个测试用法执行之后被被调用,通常用来做一些清理工作")

    def test_get_user(self):
        path = "/api/user/"
        data = {
            "code": 200
        }
        HTTP_AUTHORIZATION = "12345567"
        # 给请求头里添加信息 HTTP_AUTHORIZATION
        resp = self.client.get(path, data=data, HTTP_AUTHORIZATION=HTTP_AUTHORIZATION)
        result = resp.json()
        print(result["token"])
        self.assertEqual(result['status'], "success", result['message'])

    def test_post_user(self):
        path = "/api/user/"
        data = {
            "name": "zs",
            "age": 18
        }
        resp = self.client.post(path, data=data)
        result = resp.json()
        self.assertEqual(result['status'], "success", result['message'])

views.py

class UserView(APIView):
    def get(self, request):
        code = request.query_params.get("code")
        # 获取请求头里的信息
        token = request.META.get("HTTP_AUTHORIZATION")

        if not code:
            return Response({
                'status': "fail",
                'message': '用户未登录',
                'token': token
            })
        return Response({
            'status': "success",
            'message': 'OK',
            'token': token
        })

执行

python manage.py test  # 执行整个项目的单元测试
python manage.py test api   # 只执行api模块
python manage.py test api.tests.ApiTestCase  # 只执行某一个测试类

你可能感兴趣的:(python,Django)