跳转至

Models

定义模型

参考:https://docs.djangoproject.com/zh-hans/4.2/topics/db/models/

models.py

from django.db import models  # 引入ORM
from django.contrib.auth.models import User  # 内置的用户模型

# 每个类就是一个表,继承自 models.Model
class Exchange(models.Model):
    """
    每个属性代表一个字段(field),主键id字段不需要定义,会被自动添加
    格式:
        字段名 = models.类型(参数, "备注名")
        字段名不能用Python保留字,不能连续用多个下划线且不能以下划线结尾
        备注名默认为字段名,可自定义
    """
    exchange = models.CharField(max_length=20)

    class Meta:
        verbose_name = 'Exchange'  # 在admin后台显示的导航文案
        verbose_name_plural = '交易所'  # 复数

    def __str__(self):
        # 默认返回值,比如作为外键时,在admin后台显示为变量值,而不是关联id
        return self.exchange


class Cases(models.Model):
    """
    枚举值变量建议定义在类内部
    如果是一个易变的枚举列表,建议使用外键
    """
    ORDER_TYPE_CHOICES = [
        ("MARKET", "MARKET"),
        ("LIMIT", "LIMIT"),
        ("TWAP_ALGO", "TWAP_ALGO")
    ]

    # ForeignKey 外键
    exchange = models.ForeignKey(Exchange, on_delete=models.CASCADE)
    subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
    currency = models.ForeignKey(Currency, on_delete=models.CASCADE)

    order_type = models.CharField(
        max_length=20,  # CharField必须带max_length属性
        choices=ORDER_TYPE_CHOICES,  # 枚举值
        default="LIMIT"  # 默认值
    )
    side = models.CharField(max_length=5, default="1")
    payload = models.JSONField(blank=True, default=dict)  # 默认值可以引用一个函数,比如dict表示空字典{},默认值为{}时必须设置blank=True
    priority = models.CharField(
        max_length=5,
        choices=[("p0","P0"), ("p1","P1"), ("p2","P2"), ("p3","P3")],
        default="p0", 
        help_text="P0一般为主流币种正向用例, P1为小币种用例, P2/P3通常为边界值或异常场景用例"  # 注释
    )
    is_test = models.IntegerField(choices=[(1,"Yes"), (0,"No")], default=1)
    expect_succeed = models.IntegerField(choices=[(1,"True"), (0,"False")], default=1)
    note = models.CharField(max_length=50, blank=True)

    creator = models.ForeignKey(User, verbose_name="创建人", null=True, on_delete=models.SET_NULL)
    created_date = models.DateTimeField(verbose_name="创建日期", auto_now_add=True)
    modified_date = models.DateTimeField(verbose_name="修改日期", auto_now=True)

    class Meta:
        verbose_name = 'Exchange'

字段类型

models.AutoField(primary_key=True)  # 模型会自动设置id字段为自增主键
models.IntegerField()  # 整数
models.CharField(max_length=num)  # 字符串,必须设置 max_length
models.BooleanField()  # 布尔值
models.DateField(auto_now_add/auto_now=True)  # 日期(第一次创建时设置为现在时间/每次保存时(更新时不会)设置为现在时间)
models.DateTimeField(auto_now_add/auto_now=True)  # 日期和时间
models.JSONField(encoder=None, decoder=None)  # Json类型(v3.1版本新加,另外mysql5.7.8以下版本不支持Json类型)
models.URLField(max_length=num)  # URL
models.UUIDField()  # 唯一标识符

models.ForeignKey(想要关联的模型类名, on_delete=models.CASCADE)  # 外键(多对一关联,还可以自关联(递归))

字段选项

primary_key=True  # 设置为主键,一般不需要手动设置主键,默认会添加一列id字段为主键
unique=True  # 字段的值唯一,即不能重复

blank=True  # 字段为空时就是空
null=True  # 字段为空时设置为NULL
default=xxx  # 默认值,一般为None

help_text=xxx  # 注释文本

choices=[("S", "Small"), (B, Big)]  # 枚举值,[("存入数据库中的值", "仅用于显示")]

模型继承

三种方式参考:https://docs.djangoproject.com/zh-hans/4.0/topics/db/models/#model-inheritance

Meta 属性参考:https://docs.djangoproject.com/zh-hans/4.0/ref/models/options/

抽象基类

基类不会创建表,之类会创建表

通过在基类中设置Meta属性 abstract = True 实现

from django.db import models

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True

"""
CommonInfo将变为一个抽象基类,不会生成实际的数据表
抽象基类中的字段可以被子类继承、覆盖、也可以等于None软删除
子类也会继承Meta类,但会自动设置abstract=False,所以子类不会因为继承了基类而变成抽象基类
"""

class Student(CommonInfo):
    home_group = models.CharField(max_length=5)
    age = None  # 子类中不用这个字段

代理模型

子类与父类字段完全一致,仅仅改变行为,与基类共用一张表

通过在子类中设置Meta属性 proxy = True 实现

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

"""
至少要继承一个非抽象基类,可以继承自多个抽象父类,以及多个代理模型(但必须拥有同一个非抽象基类)
"""
class MyPerson(Person):
    class Meta:
        proxy = True

    def do_something(self):
        # ...
        pass

多表继承

子类继承自非抽象基类,子类不能覆盖基类的字段,子类与父类都会创建表

无需额外设置

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

数据迁移

  • 生成SQL语句
  • 根据生成的SQL语句创建或更新数据表
  • 版本跟踪管理
# 查看迁移状态,[x]表示已迁移
# 也可以用来检查数据库配置是否正确,有问题会报错
python manage.py showmigrations
"""
admin
 [x] 0001_initial
 [x] 0002_logentry_remove_auto_add
 [x] 0003_logentry_add_action_flag_choices
auth
 [ ] 0001_initial
 [ ] 0002_alter_permission_name_max_length
 [ ] 0003_alter_user_email_max_length
 [ ] 0004_alter_user_username_opts
 [ ] 0005_alter_user_last_login_null
 [ ] 0006_require_contenttypes_0002
 [ ] 0007_alter_validators_add_error_messages
 [ ] 0008_alter_user_username_max_length
 [ ] 0009_alter_user_last_name_max_length
 [ ] 0010_alter_group_name_max_length
 [ ] 0011_update_proxy_permissions
 [ ] 0012_alter_user_first_name_max_length
contenttypes
 [ ] 0001_initial
 [ ] 0002_remove_content_type_name
one_app
 (no migrations)
sessions
 [ ] 0001_initial
"""

# 生成迁移数据(生成SQL脚本)
python manage.py makemigrations [one_app]
"""
迁移数据存储在:one_app/migrations/0001_initial.py 中
查看生成了哪些SQL语句:python manage.py sqlmigrate one_app 0001
"""

# 执行迁移(执行SQL语句)
python manage.py migrate

# 查看创建了哪些表
"""
SQLite: .schema
MySQL: SHOW TABLES;
"""

重置迁移文件

# 查看当前迁移文件记录及状态
python manage.py showmigrations
# 将某应用的迁移文件重置为未提交状态
python manage.py migrate --fake 应用名 zero
# 然后手动删除应用下的migrations文件(内置应用存放在env/lib/python/site-packages/django/contrib下)
# 重新生成migrations文件
python manage.py makemigrations
# 重新在django_migrations表中加入记录
python manage.py migrate 应用名 --fake-initial

压缩迁移文件

# 假设要压缩的应用为myapp,现在有5个迁移文件,最新的为0005_xxx
# 生成压缩后的迁移文件
python manage.py squashmigrations 0005
# 把新的迁移文件提交下
python manage.py makemigrations
# 然后删除旧的迁移文件,把依赖旧迁移文件的地方(主要是其他应用中)都替换为新的文件名
# 删除压缩迁移的 Migration 类的 replaces 属性
# 最后执行下迁移命令不报错则说明压缩成功
python manage.py migrate

我曾经闲的蛋疼为了解决另一个坑压缩过contenttypes应用的迁移文件,使得django_migrations表中第一行记录不是contenttypes.0001_initial,导致在更换Python运行环境后执行python manage.py xxx相关的命令时,出现下面这样一个报错:

django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency contenttypes.0001_initial on database 'default'

当时查了个遍也没有得到一个满意的答案,大多数解决方式是建议必须重置数据库,但我觉得并不至于要这样做,睡了一觉之后第二天下班之后,决定静下心来自己解决掉它,最终只需要在django_migrations表中第一行补上contenttypes.0001_initial这条记录就可以了,就是这么简单。

总结

不要随便压缩并且重命名Django自带应用的数据迁移文件(内心OS:但我记得之前确实是为了改一个Django本身的问题,而那个问题是因为迁移文件的命名是数字开头,导致依赖引用的时候报了语法错误,这是不符合Python文件命名规范的,Shit)

Fixture

https://docs.djangoproject.com/en/4.2/topics/db/fixtures/

生成固定数据

可以从已有的数据库导出数据

python manage.py dumpdata app_name.ModelName > filename.json

也可以手写一些 JSON,XML 或 YAML 格式的数据放入 APP 下的 fixtures/ 路径

比如给 Person 模型提供一些数据

  • json
[
  {
    "model": "myapp.person",
    "pk": 1,
    "fields": {
      "first_name": "John",
      "last_name": "Lennon"
    }
  },
  {
    "model": "myapp.person",
    "pk": 2,
    "fields": {
      "first_name": "Paul",
      "last_name": "McCartney"
    }
  }
]
  • yaml

需要安装 PyYAML

- model: myapp.person
  pk: 1
  fields:
    first_name: John
    last_name: Lennon
- model: myapp.person
  pk: 2
  fields:
    first_name: Paul
    last_name: McCartney

使用固定数据

  • 用于测试
class MyTestCase(TestCase):
    fixtures = ["filename_a", "filename_b", "filename_c"]
  • 用于初始化数据
python manage.py loaddata filename.json

# 或者
django-admin loaddata filename.json  # 加载Json格式的数据
django-admin loaddata filename  # 加载所有格式的数据

# 可同时加载多个fixture
django-admin loaddata filename_a filename_b filename_c

数据库API

https://docs.djangoproject.com/zh-hans/4.0/ref/models/querysets/#queryset-api-reference

# 可用于admin.py或views.py
from one_app.models import Blog

# SELECT语句
Q = Blog.objects  #  Managerd对象,表级操作
Q.all()  # 等同于Q,返回一个包含所有记录的QuerySet对象集合
Q.order_by("xxx")  # 按某字段排序

# WHERE子句
r = Q.get(pk=1)  # 如果查询结果只有一条记录可以用get,但如果查不到或查到多条会报错
r.xxx  # 会重新访问数据库
r = Q.select_related("xxx").get(pk=1)
r.xxx  # 不需要重新访问数据库,性能更高

Q.filter(**kwargs)  # 过滤满足条件的记录,WHERE xxx AND xxx
Q.exclude(**kwargs)  # 排除满足条件的记录,WHERE NOT(xxx AND xxx)
Q.exclude().exclude()  # WHERE NOT xxx AND NOT xxx

# LIMIT和OFFSET子句,切片,不支持负数索引,可以用步长2
Q.all()[:5]  # LIMIT 5 返回前5个对象
Q.all()[5:10]  # OFFSET 5 LIMIT 5 返回第6~10个对象

r = Blog(Field1="x1", Field2="x2")  # 实例初始化,行级操作
r.Field1 = "x01"  # 修改字段值

r.save()  # 保存
  • 字段查询
# 完全匹配 =
Q.get(name__exact="abc")  # WHERE name = "abc";
Q.get(name="abc")  # 同上

# 比较
__gt  # 大于 greater than
__gte  # 大于等于 greater than or equal to
__lt   # 小于 less than
__lte  # 小于等于 less than or equal to
__exact  # 精确等于(区分大小写)
__iexact  # 精确等于(不区分大小写)
__contains  # 包含(区分大小写)
__icontains  # 包含(不区分大小写)
__in  # 在给定的列表中
__startswith  # 以指定字符开始(区分大小写)
__istartswith  # 以指定字符开始(不区分大小写)
__endswith  # 以指定字符结束(区分大小写)
__iendswith  # 以指定字符结束(不区分大小写)
__range  # 在两个值之间(包含边界)

# LIKE 区分大小写
Q.get(name__contains="abc")  # WHERE name LIKE '%abc%';
Q.filter(name__startswith="abc")  # WHERE name LIKE 'abc%';
Q.filter(name__endswith="abc")  # WHERE name LIKE '%abc';

# ILIKE 不区分大小写
Q.get(name__iexact="abc")  # WHERE name ILIKE 'abc';
Q.get(name__icontains="xxx")  # WHERE name ILIKE '%abc%';
Q.get(name__istartswith="abc")  # WHERE name ILIKE 'abc%';
Q.get(name__iendswith="abc")  # WHERE name ILIKE '%abc';

# IN
Q.filter(id__in=[1, 3, 4])  # WHERE id IN (1, 3, 4);

# BETWEEN...AND...
Q.filter(date__range=(start_date, end_date))  # WHERE date BETWEEN 'start_date' and 'end_date';