Django_mosh
Django mosh
shell django-admin
django-admin startproject <proj name> .
run server
python manage.py runserver
app 可以通过 proj 初始 settings.py 集成
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'new proj name',
]
创建新 app
python manage.py startapp <appname>
添加进路由
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('__debug__/', include('debug_toolbar.urls')),
path('playground', include('playground.urls')),
]
views: 类似 Controller req->res, request handler
debug: django-debug-toolbar 配置方法文档
urlconf
# --- app added
from django.urls import path
from . import views
# URLconf
urlpatterns = [
path('hello/', views.say_hello) # '/' is important
]
# --- main proj
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('__debug__/', include('debug_toolbar.urls')),
path('playground/', include('playground.urls')),
]
其中 urlpatterns 是一个内置路由变量
model 类似 entity
from django.db import models
# Create your models here.
class Product(models.Model):
title = models.CharField(max_length=255)
description = models.TextField()
# 9999.99
price = models.DecimalField(max_digits=6, decimal_places=2)
inventory = models.IntegerField()
last_update = models.DateTimeField(auto_now=True)
class Customer(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20)
birth_date = models.DateField(null=True) # nullable
注意:
- 没有 id field django 自动创建(如果没有指定主键)
类似枚举和@JsonProperty
MEMBERSHIP_BRONZE = 'B'
MEMBERSHIP_SILVER = 'S'
MEMBERSHIP_GOLD = 'G'
MEMBERSHIP_CHOICES = [
(MEMBERSHIP_BRONZE, 'Bronze'),
(MEMBERSHIP_SILVER, 'Silver'),
(MEMBERSHIP_GOLD, 'Gold'),
]
membership = models.CharField(max_length=1, choices=MEMBERSHIP_CHOICES,
default=MEMBERSHIP_BRONZE)
其中'B'是实际存在数据库之中的值,'Bronze'是实际对上呈现的值
OneToOne Mapping
class Address(models.Model):
city = models.CharField(max_length=255)
customer = models.OneToOneField(Customer, on_delete=models.CASCADE, primary_key=True)
on_delete 可以设置多种级联,例如 SETNULL,SETDEFAULT,PROTECT,CASCADE
django 里面 OneToOne 默认是双向的,会自动在 Customer 之中生成 address 属性
OneToMany: db view,直接使用 ForeignKey
e.g
class Cart(models.Model):
customer = models.OneToOneField(Customer, on_delete=models.CASCADE, primary_key=True)
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
number = models.IntegerField()
added_at = models.DateTimeField(auto_now_add=True)
ManyToMany
class Promotion(models.Model):
# 促销, 和产品是多对多关系
description = models.CharField(max_length=255)
discount = models.FloatField()
start_at = models.DateTimeField(null=True)
end_at = models.DateField(null=True)
class Product(models.Model):
title = models.CharField(max_length=255)
description = models.TextField()
# 9999.99
price = models.DecimalField(max_digits=6, decimal_places=2)
inventory = models.IntegerField()
last_update = models.DateTimeField(auto_now=True)
collection = models.ForeignKey('Collection', on_delete=models.PROTECT)
promotion = models.ManyToManyField('Promotion') # get by default 'product_set' in promotion
处理循环依赖:
Collection 类之中有一个 feature product,同时 product 被 Collection 包含,解决方法为加上''
相当于留了解析符号,但会影响修改时的改动,所以一般不要用
解决 django 自动创建反向依赖导致的重名问题,一种是指定 related_name,另一种直接指定为'+'
意思是不生成反向依赖
from django.db import models
class Collection(models.Model):
feature_product = models.ForeignKey('Product', on_delete=models.SET_NULL,null=True, related_name='+')
class Product(models.Model):
collection = models.ForeignKey(Collection, on_delete=models.PROTECT)
泛型关系
如果我们想要有一个 tag, 用于对任意物体打标签(因而需要与具体的 store 解耦),怎么做?
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
# Create your models here.
class Tag(models.Model):
title = models.CharField(max_length=255)
class TagItem(models.Model):
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
# Generic
# Type, ID -> content
content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
也就是说 content_type 入库的实际上是一个序列化后的类型信息变量(例如 string),而 object_id 是 int,运行时可以计算的结果 content_obj?
答: 是的,
content_type
字段存储的是一个 ContentType
对象的 ID,这个对象代表了关联对象的类型。ContentType
是 Django 的一个内置模型,它存储了所有已注册模型的信息,包括模型的名字和它所在的应用的名字。
object_id
字段存储的是关联对象的 ID。这个 ID 是关联对象在它所在的数据库表中的主键。
content_object
是一个 GenericForeignKey
字段,它不会在数据库中创建对应的列,而是在运行时通过 content_type
和 object_id
的值去获取关联对象。访问 content_object
属性时,Django 会根据 content_type
和 object_id
的值去查询对应的对象,并返回这个对象。
练习:LikeItem
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
# Create your models here.
class LikeItem(models.Model):
# who likes what
user = models.ForeignKey(User, on_delete=models.CASCADE)
content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
migrations
应该让 Django 完成建库建表等操作
python manage.py makemigrations
migration 是 django 自动生成的以库为单位的 db 修改操作的抽象,包含 log 等功能
修改 entity 之后也简单的重新运行 migration 就行(可能要改改文件名(生成的文件名类似 git commit 的 hashID,是会出现在后续 migration 的 depandency 里面的))
migration 只是创建了操作,之后使用如下入库
python manage.py migrate
slug
slug = models.SlugField(default='-')
slug 是一个可以匹配字母数字下划线和连字符的动态路由, 用于使一个东西对搜索引擎检索更加友好
例如,如果想检索一篇博客some-blog
,一个 url 为myweb/blogs/some-blog
的子网页肯定比myweb/blogs/1
容易检索
可以用path('.../<slug:slug>')
来动态捕获 slug 路由参数
meta data
可以在 model.Model 类的派生类之中定义一个内部类 Meta,其中就可以修改表名字,增加索引等
由于 django 一般是“约定大于配置”的逻辑,如果你在一个地方改了,就要全改来保持统一性,所以改得比较少
有了这些 migration 文件之后,可以像 git checkout 一样回到 db 的某个状态
python manage.py migrate store <migrate_no>
集成 postgresql,看官方 tutorial 改 settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mosh_course',
'USER': 'ayanami',
'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'mosh_course'),
'HOST': os.environ.get('POSTGRES_HOST', 'localhost'),
'PORT': 5432,
}
}
mock 一些数据 mockaroo.com 很好用
查询(DAO)
django 使用 queryset 进行查询,可以理解成 java 的 stream
query_set = Customer.objects.all()
query_set 可以 filter, order_by 等链式调用得到新的 query_set,并在最后一个“求值”的 api 上(例如切片,get 等操作)通过实际 query 返回数据
也就是说它是 lazy query 的,注意 query_set 不保证非 null,如果使用 get 没找到的话会抛出错误 ObjectNotFoundException,一个简单的避免抛错的方法是使用filter().first()
的形式,在 filter 返回空的情况下返回None
filter
或者get
可以传入一个特殊参数pk
来指定主键是啥
filter
只能传键值对,问题:
- 表示小于?
使用特殊的附加约定形式(类似 JPA)filter(<something>__st=5
,其中__st
的后缀作为特殊谓词标志
可以组合filter(collection__id__range=(5, 10))
collection 表中 id 为 5 ~ 10 的
常见的还有__contains
, ___startwith
条件组合 Q object 和|
,~
,&
Poll.objects.get(
Q(question__startswith="Who"),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)
基本等同于
SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')
数据引用 F Object,表示引用本表或者外键关联表里面的字段
Poll.objects.get(
Q(question__startswith="Who"),
who=F(question), # 本表的'who'字段 == 本表的 'question'字段
who=F(anothertable__question) # 本表的'who'字段 == 本表的'anothertable'外键关联表的 'question'字段
)
Sort
order_by
query_set = Customer.objects.order_by('name') #按照name字段升序
query_set = Customer.objects.order_by('-name') #按照name字段降序
得到 table 的字段对象
有不同的方法
-
values
得到 entity(object) -
values_list
得到 dict of tuple -
only
和defer
都是 entity,只查询和延迟查询,但是有个注意点:不会动态检查是否存在某个属性,如果不存在会得到非常多的 query -> 性能大幅度降低
超级牛魔的一个设计
django 默认 不 加载关联表
同时,他的更新也是 不 关联的——需要手动更新关联表内信息
如果表 A 有 B 的外键,然后加载了表 A 的 all(),就会每一个表 A 的 row 都 join 查询一次表 B
加载关联表:
all_A = A.objects.select_related('B').all()
相关的一个 api 是 prefetch,和 select 的区别在于 select 是一对一的时候用,prefetch 是一对多的时候(一个 A 可能对应多个 B 的时候)用
all_A = A.objects.prefetch_related('B').all()
aggregate
传入一个键值对,值是某种 Django 定义的回调函数(如Count('id')
,对 id 列计数)
(严格来说是 django database function 或者它的包装(使用Func
api))
返回的是{键-回调计算结果} 的 dict
ExpressionWrapper & Custom Manager
用到再说
django 会缓存
CRUD
create:
# 第一种方法,有intellisense,可以refactor
new_customer = Customer()
new_customer.birth_date = "1990-01-01"
new_customer.email = "n7sZf@example.com"
new_customer.save()
# 第二种,更短但没有
new_customer = Customer.objects.create(birth_date="1990-01-01", email="n7sZf@example.com")
update:
new_customer = Customer.objects.get(pk=1)
new_customer.birth_date = "1990-01-01"
new_customer.email = "n7sZf@example.com"
new_customer.save()
# 当然还有第二种update object.filter(pk=1).update()
注意需要用 get,也就是需要显式读
customer = Customer(pk=1)
customer.delete()
# 当然还有第二种delete object.filter(pk=1).delete()
Transaction
现在有一个原子性问题,django 是不关联更新的,所以我们要先更新 order,再更新 orderItem,这就带来了原子性问题
解决方法,使用 with transaction.atomic():
包裹多次 save()且需要保证原子性的地方
The Ultimate Solution
queryset = Customer.object.raw('SELECT * FROM store_product')
注意这个 queryset 没有 filter 之类的方法了,退化了(悲,差@Query 远甚)
admin
- custom 可以改改默认控制面板的列表名字,加上筛选和排序等等(ModelAdmin Options)
这部分其实像前端的某种统计模块了
RESTful api
1."Controller": router + json/... sender
先加个rest_framework
到 INTSALLED_APPS
然后这样
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view()
def ok(request):
return Response("ok")
路由还是在 urlpatterns 里面加
2."DTO+Serializer": serializer
有点像数据库的 api, 对不同的基本类型调用了 serializer,由于这个时候可以重新取变量名字(指定 source 属性即可)和选择丢掉的变量,就替代了@JsonProperty 和@JsonIgnore 之类的东西
from rest_framework import serializers
class customer_serializer(serializers.Serializer):
id = serializers.IntegerField()
first_name = serializers.CharField(max_length=255)
last_name = serializers.CharField(max_length=255)
email = serializers.EmailField()
phone = serializers.CharField(max_length=255)
birth_date = serializers.DateField()
之后可以这样,把 object -> serializer -> serializer.data -> dict/json response
@api_view()
def get_single_customer(request, id:int):
queryset = Customer.objects.get(id=id)
serializer = customer_serializer(queryset, many=False)
# 如果传入一个iterable就可以many=True
return Response(serializer.data)
urlpatterns = [
# dynamic route customer/{id}
path('customer/<int:id>/', views.get_single_customer),
]
上面的代码不 safe,get 一个不正确 id 就炸了
safe 写法如下,get_object_or_404
只是try except
然后返回一个Response(status=404)
的语法糖
@api_view()
def get_single_customer(request, id:int):
queryset = get_object_or_404(Customer, pk=id)
serializer = customer_serializer(queryset, many=False)
return Response(serializer.data)
这个 DTO 的部分还能给 Simplify 一点, 例如加点间接计算量
first_name = serializers.CharField(max_length=255)
last_name = serializers.CharField(max_length=255)
def get_full_name(self, customer:Customer):
return customer.first_name + " " + customer.last_name
full_name = serializers.SerializerMethodField('get_full_name')
related object serialize
serializers.PrimaryKeyRelatedField
指定queryset
(一般就直接xxx.object.all()
就行)之后返回一个 query 后的 id 列表- 也可以定义
__str__
后,serializers.StringRelatedField
- 稍微复杂的
serializers.HyperlinkedRelatedField
可以把返回的变成一个 list of url,例如 ".../collections/1"之类
如果想要对象套对象呢?
自定义一个NestObjSerializer
类,然后
nest_obj = NestObjSerializer()
DTO 的语法糖:modelSerializer
class customer_serializer(serializers.ModelSerializer):
# id = serializers.IntegerField()
# first_name = serializers.CharField(max_length=255)
# last_name = serializers.CharField(max_length=255)
# email = serializers.EmailField()
# phone = serializers.CharField(max_length=255)
# birth_date = serializers.DateField()
class Meta:
model = Customer
fields = ['id', 'first_name', 'last_name', 'email', 'phone', 'birth_date', 'full_name']
def get_full_name(self, customer:Customer):
return customer.first_name + " " + customer.last_name
full_name = serializers.SerializerMethodField('get_full_name')
POST
@api_view(['GET', 'POST'])
def single_customer(request, id:int):
if request.method == 'GET':
queryset = get_object_or_404(Customer, pk=id)
serializer = customer_serializer(queryset, many=False)
return Response(serializer.data)
elif request.method == 'POST':
serializer = customer_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(serializer.validated_data)
这里的raise_exception
相当于一个 invalidate -> return status.HTTP_404_BAD_REQUEST 的分支
可以在 serializer 之中 override validate 方法(反序列化之后是一个 Dict),例如
def validate(self, data):
if data['password']!=data['confirm_password']:
return serializers.ValidationError("xxx");
return data
入库:最简单的方法是直接serializer.save()
如果需要对 POST 传入的对象处理后再入库,可以 override serializer 的create
和update
方法
PUT 的区别在与 serializer 多传入一个被更新的 item 做参数
@api_view(['GET', 'POST', 'PUT'])
def single_customer(request, id:int):
// ...
elif request.method == 'PUT':
serializer = customer_serializer(queryset, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.validated_data)
DELETE: 对 entity 调用delete()
,手动检查有没有依赖导致无法删除的情况,如果有可以返回一个 405_method_not_allowed
Advanced API concepts
-
Class-based Views
-
Generic Views
-
ViewSet
-
Nested Routers
上面的再看
- filtering
- sorting
- paging
@api_view()
def get_sorted_customers(request):
queryset = Customer.objects.all()
first_name = request.query_params.get('first_name', None)
last_name = request.query_params.get('last_name', None)
if first_name is not None:
queryset = queryset.filter(first_name=first_name)
if last_name is not None:
queryset = queryset.filter(last_name=last_name)
serializer = customer_serializer(queryset, many=True)
return Response(serializer.data)
类视图下还可以引入外界库,django-filter 进一步简化编码,并且引入 GenericFilter
同时集成了可视化 API,支持各种过滤组合
Search: restframework 的 SearchFilter 组件
Sort: restframework 的 OrderingFilter 组件
Pagination: restframework 的 PageNumberPagination 组件