由于项目原因必须使用DjangoORM模型,所以今天整理了一下关于DjangoORM模型里的详细内容。包含字段参数、常用字段类型及参数、模型和表单验证器。
一、通用字段参数
这些参数可以应用于多种字段类型:
(1)null:如果为 True,则数据库中该字段可以存储 NULL 值。默认为 False。
(2)blank: 布尔值。如果为 True
,则表单验证允许该字段为空。默认为 False
。关键区别:blank
控制表单验证(是否允许在表单中留空),而 null
控制数据库存储(是否允许存储 NULL
值)。 它们是独立的设置。通常,对于字符串类型(CharField
、TextField
等),如果允许为空,应该同时设置 blank=True
和 null=True
。
(3)choices: 一个由二元组 (实际存储值, 显示值)
组成的序列,用于提供字段的可选项。例如:STATUS_CHOICES = [(0, '草稿'), (1, '发布')]
。在表单中会显示为下拉框。
(4)default: 字段的默认值。
(5)help_text: 在表单中显示的帮助文本,用于提供字段的说明。
(6)primary_key: 布尔值。如果为 True
,则该字段为主键。每个模型只能有一个字段设置 primary_key=True
。 通常 Django 会自动添加 AutoField
或 BigAutoField
作为主键,除非你需要自定义主键字段名或使用复合主键(不推荐)。
(7)unique: 布尔值。如果为 True
,则该字段的值在数据库中必须是唯一的。
(8)verbose_name: 字段的人类可读的名称,用于在管理后台等地方显示。如果不指定,Django 会使用字段名并把下划线转换为空格。
(9)db_column: 用于指定数据库字段的名称。通常不需要手动指定,Django 会根据字段名自动生成。只有在需要与现有数据库集成或有特殊的命名规范时才需要使用。
(10)db_index: 布尔值。如果为 True
,则在数据库中为此字段创建索引,可以加快查询速度。索引会提高查询速度,但会降低写入速度,并且会占用额外的存储空间。因此,应该只为经常用于查询的字段创建索引。
二、常用字段类型及参数
(1)AutoField: 一个自动递增的整数类型字段,通常用作主键。在 Django 3.2 之前,默认主键类型是 AutoField
,之后是 BigAutoField
。强烈建议不要手动指定主键,除非有特殊需求。
BigAutoField: 64 位整数类型字段,类似于 AutoField
,但支持更大的数值范围。Django 3.2 之后成为默认主键类型。
(2)CharField: 用于存储短字符串。
max_length
: 字符串的最大长度,必须指定。
(3)TextField: 用于存储大量文本。
没有 max_length
限制。
IntegerField、BigIntegerField、SmallIntegerField、PositiveIntegerField、PositiveSmallIntegerField: 用于存储各种大小的整数。
(4)FloatField: 用于存储浮点数。对应 SQL 的 FLOAT
或 DOUBLE
。精度可能存在问题,对于需要精确计算的场景,建议使用 DecimalField
。
(5)DecimalField: 用于存储固定精度的十进制数。
max_digits
: 数字的最大位数(包括整数部分和小数部分)。
decimal_places
: 小数位数。
(6)BooleanField: 用于存储布尔值(True
或 False
)。对应 SQL 的 TINYINT(1)
或 BOOLEAN
。
(7)NullBooleanField:已完全弃用,应该使用 BooleanField(null=True)
。
(8)DateField: 用于存储日期。
auto_now
: 每次保存对象时自动更新为当前日期。通常用于记录修改时间。
auto_now_add
: 对象第一次创建时自动设置为当前日期。通常用于记录创建时间。
auto_now
和 auto_now_add
互斥,不能同时使用。
(9)DateTimeField: 用于存储日期和时间。
auto_now
: 每次保存对象时自动更新为当前日期和时间。
auto_now_add
: 对象第一次创建时自动设置为当前日期和时间。
auto_now
和 auto_now_add
互斥,不能同时使用。
(10)TimeField: 用于存储时间。
(11)DurationField: 用于存储时间间隔。
(12)EmailField: 用于存储电子邮件地址。会对输入进行基本的电子邮件格式验证。
(13)FileField: 用于存储文件。
upload_to
: 指定文件上传的目录。强烈推荐使用可调用对象(函数或方法),以便更灵活地组织文件存储路径,例如按日期或用户ID等。
max_length
: 文件路径的最大长度。
(14)ImageField: 用于存储图像文件。继承自 FileField
。需要安装 Pillow 库 (pip install Pillow
)。
(15)URLField: 用于存储 URL。会对输入进行基本的 URL 格式验证。
(16)UUIDField: 用于存储 UUID。
(17)GenericIPAddressField: 用于存储 IPv4 或 IPv6 地址。
protocol
: 指定协议('both'
、'IPv4'
或 'IPv6'
)。
unpack_ipv4
: 如果为 True
,则将 IPv4 地址解包为 IPv6 格式。
(18)ForeignKey: 用于定义多对一关系。
to
: 关联的模型。
on_delete
: 非常重要! 指定删除关联对象时的行为:
models.CASCADE
: 级联删除。
models.PROTECT
: 阻止删除。
models.SET_NULL
: 设置为 NULL
,必须配合 null=True
使用。
models.SET_DEFAULT
: 设置为默认值,必须配合 default
参数使用。
models.DO_NOTHING
: 什么也不做(不推荐,容易造成数据不一致)。
models.SET(value)
: 设置为指定的值 value
。value
也可以是一个可调用对象。
related_name
: 反向关联的名称。强烈推荐显式指定,提高代码可读性。
related_query_name
: 用于反向查询时的查询名称。
db_constraint
: 是否在数据库中创建外键约束,默认为 True
。通常保持默认值。
(19)ManyToManyField: 用于定义多对多关系。
through
: 指定中间模型。当需要在关系中存储额外数据(例如关系创建时间)时,必须使用 through
指定中间模型。
db_table
: 指定数据库表的名称。通常不需要指定,Django 会自动生成。
parent_link
: 仅用于多表继承的情况,表示该字段是父模型的链接。并非所有 ManyToManyField
都会用到。
(20)OneToOneField: 用于定义一对一关系。本质上就是一个 unique=True
的 ForeignKey
。
再次强调的重要点:
理解 blank
和 null
的区别及其组合使用方式。
on_delete
参数至关重要,根据业务逻辑谨慎选择。
强烈建议为 ForeignKey
和 ManyToManyField
指定 related_name
。
NullBooleanField
已完全弃用。
Django 3.2 之后,默认主键类型是 BigAutoField
。
auto_now
和 auto_now_add
互斥。
对于浮点数精确计算,使用 DecimalField
。
结合上面通用字段参数和常用字段类型及参数编写几个关于电商平台的表:
import uuid
from django.db import models
from django.core.exceptions import ValidationError
from django.db.models import F
from django.utils.translation import gettext_lazy as _class Category(models.Model):"""商品分类"""name = models.CharField(_("分类名称"), max_length=50, unique=True)description = models.TextField(_("分类描述"), blank=True, null=True)def __str__(self):return self.nameclass Meta:verbose_name = _("商品分类")verbose_name_plural = _("商品分类") # 添加复数形式class Tag(models.Model):"""商品标签"""name = models.CharField(_("标签名称"), max_length=20, unique=True)def __str__(self):return self.nameclass Meta:verbose_name = _("商品标签")verbose_name_plural = _("商品标签")class Product(models.Model):"""商品信息"""STATUS_CHOICES = [(0, _('下架')),(1, _('上架')),]id = models.UUIDField(_("商品ID"), primary_key=True, default=uuid.uuid4, editable=False)name = models.CharField(_("商品名称"), max_length=100, db_index=True)description = models.TextField(_("商品描述"), blank=True, null=True)price = models.DecimalField(_("商品价格"), max_digits=14, decimal_places=2)stock = models.PositiveIntegerField(_("商品库存"), default=0)created_at = models.DateTimeField(_("创建时间"), auto_now_add=True)updated_at = models.DateTimeField(_("更新时间"), auto_now=True)status = models.IntegerField(_("商品状态"), choices=STATUS_CHOICES, default=1)category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products', verbose_name=_("商品分类"))tags = models.ManyToManyField(Tag, related_name='products', blank=True, verbose_name=_("商品标签"))image = models.ImageField(_("商品图片"), upload_to='products/', blank=True, null=True)def __str__(self):return self.nameclass Meta:verbose_name = _("商品")verbose_name_plural = _("商品")ordering = ['-created_at']class Order(models.Model):"""订单信息"""STATUS_CHOICES = [(0, _('待付款')),(1, _('已付款')),(2, _('已发货')),(3, _('已完成')),(4, _('已取消')),]order_number = models.CharField(_("订单编号"), max_length=20, unique=True)created_at = models.DateTimeField(_("下单时间"), auto_now_add=True)status = models.IntegerField(_("订单状态"), choices=STATUS_CHOICES, default=0)def __str__(self):return self.order_numberclass Meta:verbose_name = _("订单")verbose_name_plural = _("订单")class OrderItem(models.Model):"""订单条目"""order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items', verbose_name=_("所属订单"))product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name='order_items', verbose_name=_("商品"))quantity = models.PositiveIntegerField(_("购买数量"))price = models.DecimalField(_("购买时价格"), max_digits=14, decimal_places=2)def clean(self):if self.quantity > self.product.stock:raise ValidationError({'quantity': _('库存不足')}) # 翻译错误信息def save(self, *args, **kwargs):# 使用 F() 表达式原子性地更新库存,避免并发问题if self.pk is None: #仅在创建新OrderItem时更新库存Product.objects.filter(pk=self.product.pk).update(stock=F('stock') - self.quantity)super().save(*args, **kwargs)class Meta:verbose_name = _("订单条目")verbose_name_plural = _("订单条目")# 移除信号处理,使用 F() 表达式更高效和安全
# @receiver(pre_save, sender=OrderItem)
# def check_stock(sender, instance, **kwargs):
# if instance.quantity > instance.product.stock:
# raise ValidationError({'quantity': '库存不足'})
# 关于库存管理的更详细解释:
# 使用 F() 表达式是解决并发环境下库存管理的最佳实践。假设有两个用户同时购买同一件商品,如果使用 Python 代码先读取库存,然后减去购买数量再保存,就可能出现以下情况:
# 用户 A 读取库存,假设为 10。
# 用户 B 读取库存,此时库存仍然为 10。
# 用户 A 购买 5 件,库存更新为 5。
# 用户 B 购买 5 件,库存更新为 5。
# 结果是两个用户都购买了 5 件,总共购买了 10 件,而实际上库存只减少了 5 件,导致超卖。
三、模型和表单验证器
(1)数值验证器:
MaxValueValidator(limit_value): 验证值是否小于或等于 limit_value
。
MinValueValidator(limit_value): 验证值是否大于或等于 limit_value
。
DecimalValidator(max_digits, decimal_places): 验证十进制数的位数和精度。max_digits
是总位数(包括小数位),decimal_places
是小数位数。
IntegerValidator: 验证值是否为整数。
(2)字符串验证器:
EmailValidator(message=None, code=None, allowlist=None): 验证值是否为有效的电子邮件地址。allowlist
参数允许指定额外的域名白名单。
RegexValidator(regex, message=None, code=None, inverse_match=None, flags=0): 使用正则表达式 regex
验证值。inverse_match
参数用于反向匹配(即值 不 应该匹配正则表达式)。flags
参数用于设置正则表达式的标志(例如 re.IGNORECASE
)。
URLValidator(schemes=None, message=None, code=None): 验证值是否为有效的 URL。schemes
参数允许指定允许的 URL 协议列表(例如 ['http', 'https']
)。
MaxLengthValidator(limit_value): 验证字符串的最大长度。
MinLengthValidator(limit_value): 验证字符串的最小长度。
(3)文件验证器:
FileExtensionValidator(allowed_extensions, message=None, code=None): 验证上传文件的扩展名是否在 allowed_extensions
列表中。
(4)其他验证器和验证函数:
validate_email(value): 验证值是否为有效的电子邮件地址。(功能与 EmailValidator
类似,但可以直接作为函数使用。)
validate_slug(value): 验证值是否为有效的 slug(URL 中使用的短标签,通常只包含字母、数字、连字符和下划线)。
validate_unicode_slug(value): 验证值是否为有效的 Unicode slug。
validate_ipv4_address(value): 验证值是否为有效的 IPv4 地址。
validate_ipv6_address(value): 验证值是否为有效的 IPv6 地址。
validate_ipv46_address(value): 验证值是否为有效的 IPv4 或 IPv6 地址。
(5)使用方法:
这些验证器可以用于模型字段和表单字段。
(6)模型字段: 将验证器实例作为列表传递给字段的 validators
参数。
(7)表单字段: 同样,将验证器实例列表传递给字段的 validators
参数。
(8)自定义验证器:
除了内置验证器,你还可以创建自定义验证器。自定义验证器是一个可调用对象(例如函数或带有 __call__
方法的类),它接收一个值,并在值无效时引发 django.core.exceptions.ValidationError
异常。
结合上面模型和表单验证器和上面的案例重新编写关于电商平台的表:
import uuid
from django.db import models, transaction
from django.db.models import F
from django.utils.translation import gettext_lazy as _
from django.core.validators import (MaxValueValidator,MinValueValidator,DecimalValidator,RegexValidator,URLValidator,MaxLengthValidator,MinLengthValidator,FileExtensionValidator,validate_email,validate_slug,
)
from django import formsclass Category(models.Model):"""商品分类"""name = models.CharField(_("分类名称"), max_length=50, unique=True)description = models.TextField(_("分类描述"), blank=True, null=True)def __str__(self):return self.nameclass Meta:verbose_name = _("商品分类")verbose_name_plural = _("商品分类")class Tag(models.Model):"""商品标签"""name = models.CharField(_("标签名称"), max_length=20, unique=True)def __str__(self):return self.nameclass Meta:verbose_name = _("商品标签")verbose_name_plural = _("商品标签")class Product(models.Model):"""商品信息"""STATUS_CHOICES = [(0, _('下架')),(1, _('上架')),]id = models.UUIDField(_("商品ID"), primary_key=True, default=uuid.uuid4, editable=False)name = models.CharField(_("商品名称"), max_length=100, db_index=True, validators=[MinLengthValidator(2)])description = models.TextField(_("商品描述"), blank=True, null=True)price = models.DecimalField(_("商品价格"), max_digits=14, decimal_places=2, validators=[MinValueValidator(0)])stock = models.PositiveIntegerField(_("商品库存"), default=0) # 移除不必要的MaxValueValidator,或根据实际业务调整created_at = models.DateTimeField(_("创建时间"), auto_now_add=True)updated_at = models.DateTimeField(_("更新时间"), auto_now=True)status = models.IntegerField(_("商品状态"), choices=STATUS_CHOICES, default=1)category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products', verbose_name=_("商品分类"))tags = models.ManyToManyField(Tag, related_name='products', blank=True, verbose_name=_("商品标签"))image = models.ImageField(_("商品图片"), upload_to='products/', blank=True, null=True, validators=[FileExtensionValidator(['jpg', 'jpeg', 'png', 'gif'])])def __str__(self):return self.nameclass Meta:verbose_name = _("商品")verbose_name_plural = _("商品")ordering = ['-created_at']class Order(models.Model):"""订单信息"""STATUS_CHOICES = [(0, _('待付款')),(1, _('已付款')),(2, _('已发货')),(3, _('已完成')),(4, _('已取消')),]order_number = models.CharField(_("订单编号"), max_length=20, unique=True, db_index=True, validators=[RegexValidator(r'^[A-Za-z0-9-]+$', _('订单编号只能包含字母、数字和连字符'))])created_at = models.DateTimeField(_("下单时间"), auto_now_add=True)status = models.IntegerField(_("订单状态"), choices=STATUS_CHOICES, default=0)def __str__(self):return self.order_numberclass Meta:verbose_name = _("订单")verbose_name_plural = _("订单")class OrderItem(models.Model):"""订单条目"""order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items', verbose_name=_("所属订单"))product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name='order_items', verbose_name=_("商品"))quantity = models.PositiveIntegerField(_("购买数量"), validators=[MinValueValidator(1)])price = models.DecimalField(_("购买时价格"), max_digits=14, decimal_places=2, validators=[MinValueValidator(0)])def save(self, *args, **kwargs):with transaction.atomic():if self.pk is None:Product.objects.filter(pk=self.product.pk).update(stock=F('stock') - self.quantity)else:old_item = OrderItem.objects.get(pk=self.pk)stock_diff = old_item.quantity - self.quantityProduct.objects.filter(pk=self.product.pk).update(stock=F('stock') + stock_diff)super().save(*args, **kwargs)def delete(self, *args, **kwargs):with transaction.atomic():Product.objects.filter(pk=self.product.pk).update(stock=F('stock') + self.quantity)super().delete(*args, **kwargs)class Meta:verbose_name = _("订单条目")verbose_name_plural = _("订单条目")class OrderItemForm(forms.ModelForm):class Meta:model = OrderItemfields = '__all__'def clean_quantity(self):quantity = self.cleaned_data['quantity']product = self.cleaned_data['product']if quantity > product.stock:raise forms.ValidationError(_('库存不足'))return quantity