Skip to content

Latest commit

 

History

History
478 lines (385 loc) · 17 KB

2.模型定义规范.md

File metadata and controls

478 lines (385 loc) · 17 KB

模型定义规范

1.扩展的字段类型

  • 主要解决使用django自带ORM字段类型,migrate建表时与mysql字段类型规范不符或缺失

  • 使用原生SQL语句建表则可以忽略

  • 扩展的字段类型

    • FixedCharField
      • django.db.models.CharField 对应mysql的 varchar,这里扩展出char类型
    • BinaryFixCharField
      • 区分大小写的char,对应mysql字段定义语句 char(n) binary
    • BinaryCharField
      • 区分大小写的varchar,对应mysql字段定义语句 varchar(n) binary
    • TextField
      • django.db.models.TextField 对应mysql的 longtext,这里扩展出text类型
    • LongTextField
      • django.db.models.TextField 的别名,命名更严谨
    • TinyIntField
      • 参考 django.db.models.BigIntegerField,扩展出tinyint类型
  • 代码

# 文件位置:libs/django/db/models.py

# coding: utf-8

from django.db import models
from django.utils.translation import gettext_lazy as _


class FixedCharField(models.Field):
    """char类型"""

    description = _("Char")

    def db_type(self, connection):
        """
        限定生成数据库表的字段类型为 char,长度为 max_length 指定的值
        """
        return 'char(%s)' % self.max_length


class BinaryFixCharField(models.Field):
    """char binary 类型"""

    description = _("Char Binary")

    def db_type(self, connection):
        return 'char(%s) binary' % self.max_length


class BinaryCharFiled(models.Field):
    """varchar binary类型"""

    description = _('Varchar Binary')

    def db_type(self, connection):
        return 'varchar(%s) binary' % self.max_length


class TextField(models.TextField):
    """text类型"""

    description = _('Text')

    def db_type(self, connection):
        return 'text'


LongTextField = models.TextField


class TinyIntField(models.IntegerField):
    """tinyint类型"""

    description = _('Tiny (1 byte) integer')
    MAX_TINYINT = 127

    def formfield(self, **kwargs):
        return super().formfield(**{
            'min_value': -TinyIntField.MAX_TINYINT - 1,
            'max_value': TinyIntField.MAX_TINYINT,
            **kwargs,
        })

    def db_type(self, connection):
        return 'tinyint'

2.扩展的枚举类(Django>=3.0 后推荐使用:models.TextChoices

  • 表示xxx类型字段,使用枚举类中定义的字段代替手动指定值,减少误操作
  • 支持choices设置
  • 代码
# 文件位置:libs/django/db/enum.py

# coding: utf-8

from enum import Enum, unique
from types import DynamicClassAttribute


def first_value_unique(enumeration):
    """装饰器:序列的第一个元素唯一"""
    
    unique(enumeration)
    number_set = set()
    for elem in enumeration:
        first_value = elem.value[0]
        if first_value in number_set:
            raise ValueError('duplicate first value found in %r' % elem)
        number_set.add(first_value)
    return enumeration


class TypeFieldEnum(Enum):
    """类型字段枚举类

    使用demo:
        @first_value_unique
        class XxxEnum(TypeFieldEnum):
            
            # 元组格式:(数据库中存储的值, 展示内容)
            XXX1 = (1, '展示内容1')
            XXX2 = (2, '展示内容2')
            ...
        
        Model.objects.create(xxx_type=XxxEnum.XXX1.val)
    """

    @classmethod
    def to_tuple(cls):
        """转为元组"""
        return tuple(item.value for item in cls)

    @DynamicClassAttribute
    def val(self):
        """值"""
        return self.value[0]

    @DynamicClassAttribute
    def display(self):
        """展示"""
        return self.value[1]

3.常用字段类型

  • django.db.models.BigAutoField
    • 以自增bigint为主键
    • primary_key=True
  • libs.django.db.models.TinyIntField
    • 对应mysql tinyint 类型
    • 存储范围:-128 ~ 127 (1字节)
    • 建议表示类型字段使用此类型
  • django.db.models.IntegerField
    • 对应mysql int 类型
    • 存储范围:-2^31 ~ 2^31-1 (4字节)
  • django.db.models.BigIntegerField
    • 对应mysql bigint 类型
    • 存储范围:-2^63 ~ 2^63-1 (8字节)
  • libs.django.db.models.FixedCharField
    • 对应mysql char 类型
    • max_length对应char的长度
  • django.db.models.CharField
    • 对应mysql varchar 类型
    • 建议设置null=False, default=''
  • libs.django.db.models.BinaryFixCharField
    • 对应mysql字段定义语句 char(n) binary,实质是使用该表定义的字符集下,_bin后缀的校对规则,如 utf8mb4_bin
    • 区分大小写
  • django.db.models.DateField
    • 对应mysql date 类型
  • django.db.models.DateTimeField
    • 对应mysql datetime(6) 类型
    • auto_now_add=True,模型创建时自增添加当前时间
    • auto_now=True,模型创建或更新时添加当前时间
      • 注意:QuerySet.update() 为直接执行SQL语句,不涉及模型对象操作,需要手动指定更新时间
  • django.db.models.BooleanField
    • 对应mysql tinyint 类型
    • 建议设置default=0或1
    • 通常建议每个表固定定义一个 is_deleted 字段,作逻辑删除用
  • django.db.models.DecimalField
    • 绝对精度类型,对应mysql decimal(M, D) 类型
    • max_digits <==> M,表示数字总个数
    • decimal_places <==> D,表示小数位的个数
    • 建议金钱相关字段使用
  • django.db.models.FloatField
    • 低精度,禁止使用
  • libs.django.db.models.TextField
    • 对应mysql text 类型
    • 文本存储考虑使用
  • django.db.models.TextField
    • 对应mysql longtext 类型
    • 命名不严谨,不推荐使用
  • libs.django.db.models.LongTextField
    • 对应mysql longtext 类型,
    • 为django.db.models.TextField的别名,命名更严谨
    • 超大文本存储考虑使用
  • django_jsonfield_backport.models.JSONField
    • 三方模块
    • 对应mysql json 类型
    • 不推荐使用 jsonfield 三方模块
      • migrate 创建的类型为mysql longtext
  • 外键字段
    • django.db.models.OneToOneField
      • 一对一外键(源表唯一键)使用
      • db层面会自动添加唯一索引
      • 必须加 db_constraint=False,避免建立数据库层面的外键约束
      • on_delete由于实际场景为逻辑删除,不会出现delete,不做要求
    • django.db.models.ForeignKey
      • 多对一外键使用
      • db层面会自动添加二级索引
      • 必须加 db_constraint=False,避免建立数据库层面的外键约束
      • on_delete由于实际场景为逻辑删除,不会出现delete,不做要求
    • django.db.models.ManyToManyField
      • 自动生成中间表,不可控,禁止使用
      • 推荐自定义中间表,指定 django.db.models.ForeignKey

4.常用字段属性(参数)

  • primary_key: bool
    • 指定db层面主键
  • verbose_name: str
    • django层面的字段注释,不会应用到mysql comment
  • max_length: int
    • 字符串类型指定db层面存储字符个数上限,django层面也会做校验
  • null: bool
    • 指定db层面字段可否为 NULL ,django层面也会做校验
    • 通常情况,推荐设置null=False
    • 大字段类型,统一设置null=True
  • default: Union[Any, Callable]
    • 仅django层面,在模型实例化时触发
    • 默认值,可以传入函数,模型定义时会调用 __call__ 方法
  • unique: bool
    • db层面添加唯一索引,索引名称不可控
  • db_index: bool
    • db层面添加二级索引,索引名称不可控
  • auto_now_add: bool
    • 日期类型,新增模型,model_obj.save() 时触发
    • 区别与 default=datetime.datetime.now,触发时机不同
  • auto_now: bool
    • 日期类型,model_obj.save() 时触发
    • QuerySet.update() 时是直接执行SQL,不涉及模型的变化,不会触发,需要手动指定时间
  • choices: Sequence[Tuple[Any, Any]]
    • 每个元组的第一个值为数据库中实际存储的值,第二个值为显示内容
    • 定义后可以使用 model_obj.get_字段名_display() 获取显示内容
  • DecimalField字段属性
    • max_digits: int
      • 表示数值的总个数(小数部分+整数部分)
    • decimal_places: int
      • 表示小数部分的数值个数
  • 外键字段属性
    • to: Union[Model, str]
      • 关联的模型
    • to_field: Optional[str] = None
      • 关联模型的字段,默认使用主键
    • on_delete: Callable
      • 依赖字段数据删除(delete)时触发的操作,由于实际业务场景都是逻辑删除(update),所以不做特殊要求
      • models.CASCADE
        • 级联删除
      • models.PROTECT
        • 抛出异常,不可删除
      • models.DO_NOTHING
        • 无操作
      • models.SET_NULL
        • 与之关联的值设置为null,必须null=True
    • db_constraint: bool = True
      • 是否创建mysql层面的外键,必须设置为False

5.Meta类常用属性

  • db_table: str
    • 对应db层面的表名
  • verbose_name: str
    • django层面的表注释,不会应用到mysql层面的表注释
  • unique_together: Sequence[str]
    • mysql层面会创建联合唯一索引,推荐使用元组
  • index_together: Sequence[str]
    • mysql层面会创建联合二级索引,推荐使用元组

6.模型定义示例

# app_xxx/models.py

# coding: utf-8

import datetime
from django.db import models
from django_jsonfield_backport.models import JSONField

from libs.django.db.models import FixedCharField, BinaryCharFiled, TinyIntField, TextField, LongTextField
from libs.django.db.enum import TypeFieldEnum, first_value_unique
from libs.uuid import make_uuid

__all__ = (
    'Xxx',
    'XxxDetail',
    'Yyy'
)


class Xxx(models.Model):

    class Meta:
    
        # 实际表名以 `t_` 为前缀
        db_table = 't_xxx'
        # ORM表注释
        verbose_name = 'xxx表'
        # 联合索引,仅展示功能,无实际意义
        index_together = ('xxx_type', 'xxx_name')

    @first_value_unique
    class XxxTypeEnum(TypeFieldEnum):
        """xxx类型枚举类"""

        TYPE_ONE = (1, '类型1')
        TYPE_TWO = (2, '类型2')
        TYPE_THREE = (3, '类型3')

    # 主键自增id,要求使用bigint且业务无关
    id = models.BigAutoField(primary_key=True)
    # xxx_id使用uuid,固定32位长度,唯一键
    xxx_id = FixedCharField(verbose_name='xxxid', max_length=32, null=False, default=make_uuid, unique=True)
    # 类型字段建议使用tinyint,针对choices扩展出枚举类,方便管理
    xxx_type = TinyIntField(verbose_name='xxx类型', null=False, default=XxxTypeEnum.TYPE_ONE.val,
                            choices=XxxTypeEnum.to_tuple())
    # varchar类型统一定义成 not null default ''
    xxx_name = models.CharField(verbose_name='xxx名称', max_length=50, null=False, default='')
    # varchar binary,区分大小写
    xxx_name_bin = BinaryCharFiled(verbose_name='xxx名称(区分大小写)', max_length=50, null=False, default='')
    # 每个表必加的三个字段
    # 是否已删除,使用ORM布尔类型,对应mysql的tinyint
    is_deleted = models.BooleanField(verbose_name='是否已删除', null=False, default=0)
    # 模型创建时自动更新, model_obj.save()
    create_time = models.DateTimeField(verbose_name='创建时间', null=False, auto_now_add=True)
    # 模型修改时自动更新, model_obj.save(),注意:直接update时不触发
    update_time = models.DateTimeField(verbose_name='更新时间', null=False, auto_now=True)

    """生成的表结构
    CREATE TABLE `t_xxx` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `xxx_id` char(32) NOT NULL,
      `xxx_type` tinyint(4) NOT NULL,
      `xxx_name` varchar(50) NOT NULL,
      `xxx_name_bin` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
      `is_deleted` tinyint(1) NOT NULL,
      `create_time` datetime(6) NOT NULL,
      `update_time` datetime(6) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `xxx_id` (`xxx_id`),
      KEY `t_xxx_xxx_type_xxx_name_1318ffcb_idx` (`xxx_type`,`xxx_name`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4  
    """


class XxxDetail(models.Model):
    """xxx子表(详情表)

    有些情况,需要将大字段从源表中垂直拆分出来,形成一个子表,以减少查询I/O
    """

    class Meta:

        db_table = 't_xxx_detail'
        verbose_name = 'xxx详情表'

    id = models.BigAutoField(primary_key=True)
    '''一对一外键(源表唯一键)使用 `models.OneToOneField`
        外键字段名到mysql中会自动加上 `_id`,这里实际字段名为 `xxx_id` 
        外键 to='Xxx' 一律使用源表模型类的字符串形式,不允许出现 to=Xxx,避免出现定义先后顺序不同导致报错
        必须加 db_constraint=False,避免建立数据库层面的外键约束
        on_delete由于实际场景为逻辑删除,不会出现delete,不做要求
    '''
    xxx = models.OneToOneField('Xxx', to_field='xxx_id', verbose_name='xxxid', on_delete=models.CASCADE, db_constraint=False)
    '''大字段类型
        django自带的TextField为mysql的longtext,这里使用真实的TextField
        大字段类型统一设置 null=True
    '''
    content = TextField(verbose_name='内容', null=True)
    long_content = LongTextField(verbose_name='长内容', null=True)
    name_list = JSONField(verbose_name='名称列表', null=True)

    """生成的表结构
    CREATE TABLE `t_xxx_detail` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `content` text,
      `long_content` longtext,
      `name_list` json DEFAULT NULL,
      `xxx_id` char(32) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `xxx_id` (`xxx_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    """


class Yyy(models.Model):

    class Meta:

        db_table = 't_yyy'
        verbose_name = 'yyy表'

    id = models.BigAutoField(primary_key=True)
    yyy_id = FixedCharField(verbose_name='yyyid', max_length=32, null=False, default=make_uuid, unique=True)
    '''多对一外键使用 `models.ForeignKey`
        外键 to='Xxx' 一律使用源表模型类的字符串形式,不允许出现 to=Xxx,避免出现定义先后顺序不同导致报错
        必须加 db_constraint=False,避免建立数据库层面的外键约束
        on_delete由于实际场景为逻辑删除,不会出现delete,不做要求
        null值的定义:
            True:  关联查询默认使用 INNER JOIN
            False: 关联查询默认使用 LEFT JOIN
    '''
    xxx = models.ForeignKey('Xxx', to_field='xxx_id', verbose_name='xxxid', null=False, on_delete=models.CASCADE, db_constraint=False)
    is_deleted = models.BooleanField(verbose_name='是否已删除', null=False, default=0)
    create_time = models.DateTimeField(verbose_name='创建时间', null=False, auto_now_add=True)
    update_time = models.DateTimeField(verbose_name='更新时间', null=False, auto_now=True)

    """生成的表结构
    CREATE TABLE `t_yyy` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `yyy_id` char(32) NOT NULL,
      `is_deleted` tinyint(1) NOT NULL,
      `create_time` datetime(6) NOT NULL,
      `update_time` datetime(6) NOT NULL,
      `xxx_id` char(32) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `yyy_id` (`yyy_id`),
      KEY `t_yyy_xxx_id_ef705c68` (`xxx_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    """

7.模型类中定义属性,必须使用 udp_ 前缀,避免后续新增字段引起冲突

  • udp:User Define Property
from django.utils.functional import cached_property


class Member(models.Model):
    
    id = models.BigAutoField(primary_key=True)
    ...


class RelDeptMember(models.Model):
    id = models.BigAutoField(primary_key=True)
    member = models.ForeignKey("Member", verbose_name="成员id", null=False, on_delete=models.CASCADE, db_constraint=False)
    dept = models.ForeignKey("Dept", verbose_name="部门id", null=False, on_delete=models.CASCADE, db_constraint=False, 
                             related_name="rel_rdm_dept")
    is_deleted = models.BooleanField(verbose_name='是否已删除', null=False, default=0)
    create_time = models.DateTimeField(verbose_name='创建时间', null=False, auto_now_add=True)
    update_time = models.DateTimeField(verbose_name='更新时间', null=False, auto_now=True)
    

class Dept(models.Model):
    
    id = models.BigAutoField(primary_key=True)
    ...
    
    @cached_property
    def udp_member_num(self):
        """成员数"""
        return self.rel_rdm_dept.filter(is_deleted=0).count()
    

8.模型类中定义属性(自定义字段),必须使用 @cached_property 装饰

  • from django.utils.functional import cached_property
  • cached_property 是property的超集,当 get 时,会缓存数据,避免多次重复执行
  • 示例见7