Django 3.0 开始已经默认支持枚举类型了,使用的方法是通过给模型的内部增加一个元组或内部类代表枚举的选项。参考官方文档和我在文章最下方的代码。在CharField
或IntegerField
的构造函数增加一个choices
参数,指定代表枚举的元组常数或内部类成员。但是这种方法有个缺陷就是,仅仅在前端实现了枚举类型,但是在代码层面,无法阻止枚举选项之外的数据存入。在数据库(例如mysql)层面,该枚举类型模型仍然只是一个VARCHAR()
。
若是想要在Django中使用Mysql的枚举(ENUM)类型,需要安装一个Django的Mysql扩展,名为django-mysql 项目地址。
安装
参考官方的文档,安装方法为:
python -m pip install django-mysql
很简单,就如同安装其他python第三方包一样。然后还需在setting.py
中的INSTALLED_APPS
栏填入django_mysql
,注意在代码中要用下划线而非横杠
INSTALLED_APPS = (
...
'django_mysql',#注意在代码中要用下划线而非横杠
)
使用
假设我们创建一个用户模型,用户的状态status
是枚举类型,可以这样实现。
测试的环境为linux,python 3.8.6,django 3.1.7。
类似以前的方法,需要先创建出一个含有枚举选项元组常数STATUS_LIST
。然后使用django_mysql.models
模块提供的EnumField
,在构造函数的choices
参数传入元组常数。元组常数的每一个选项的第一项都是存入数据库的数据形式,后一项是在前端展示的可读形式(human readable)
from django.db import models
import django_mysql.models
class User0(models.Model):
username = models.CharField(max_length=20, verbose_name='用户名')
STATUS_LIST = (
('A', '正常'),
('B', '封禁中'),
('C', '已注销'),
)
status = django_mysql.models.EnumField(choices=STATUS_LIST)
def __str__(self):
return self.username
在迁移后,查看数据库的类型,是真正的MySQLENUM
类型,而非通过VARCHAR
模拟。
+----------+-------------------+-
| Field | Type |
+----------+-------------------+-
| id | int |
| username | varchar(20) |
| status | enum('A','B','C') |
+----------+-------------------+-
访问Django的管理页面,可以在前端看到可读形式的选项
数据库内查表可见已经存入数据了
+----+----------+--------+
| id | username | status |
+----+----------+--------+
| 1 | yizdu | B |
+----+----------+--------+
若在代码层面尝试存入非法数据
In [6]: from temp_test.models import *
In [7]: u0=User0()
In [8]: u0.status='X'
In [9]: u0.save()
将会引发MySQL的数据错误
DataError: (1265, "Data truncated for column 'status' at row 1")
注意事项
在官方的文档中有警告
可以在迁移中将新值附加到choices
中,也可以编辑现有选项的可读名称。
但是,如果启用了MySQL严格模式,则编辑或删除现有选择值将出错,如果未启用,则将其替换为空字符串。
同样,空字符串在ENUM
上的行为也很奇怪,有点像NULL
,但不完全是这样。 因此建议您启用严格模式。
实践中我发现,如果先使用了Django默认的枚举实现方式,再修改为使用django-mysql扩展的实现方式,在迁移时将会出错。我的解决方法是删表重建。
首先在数据库先将模型对应的表删除,然后在models.py中注释掉该模型,然后执行
python manage.py makemigrations
python manage.py migrate --fake
随后取消注释,用django-mysql的方式实现功能,执行以下命令重新迁移
python manage.py makemigrations
python manage.py migrate
其他枚举方式实现参考
在Django的默认枚举实现中,官方文档推荐采用第二种。如果需要在多个模型里使用相同的枚举字段,可以将元组常数放到全局,或将内部类转为和模型同级的类。
from django.db import models
#通过创建一个枚举元组常量
class User1(models.Model):
username = models.CharField(max_length=20, verbose_name='用户名')
STATUS_LIST = (
('A', '正常'),
('B', '封禁中'),
('C', '已注销'),
)
status = models.CharField(
max_length=1, verbose_name='状态', choices=STATUS_LIST)
def __str__(self):
return self.username
#通过创建一个内部类
class User2(models.Model):
username = models.CharField(max_length=20, verbose_name='用户名')
class StatusList(models.TextChoices):
ACTIVITY = 'A', '正常'
BANNED = 'B', '封禁中'
CLOSE = 'C', '已注销'
status = models.CharField(
max_length=1, verbose_name='状态', choices=StatusList.choices)
def __str__(self):
return self.username
正常使用示例:
In [1]: from temp_test.models import *
In [2]: u1=User1()
In [3]: u1.username='yizdu'
In [4]: u1.status='A'
In [5]: u1.save()
In [6]: u2=User2()
In [8]: u2.username='yizdu'
In [9]: u2.status=User2.StatusList.ACTIVITY
In [10]: u2.save()
数据库内
mysql > SELECT * FROM temp_test_user1;
+----+----------+--------+
| id | username | status |
+----+----------+--------+
| 2 | yizdu | A |
+----+----------+--------+
2 rows in set
Time: 0.022s
mysql > SELECT * FROM temp_test_user2;
+----+----------+--------+
| id | username | status |
+----+----------+--------+
| 1 | yizdu | A |
+----+----------+--------+
查看表中数据类型,status
字段通过VARCHAR()
储存
# DESC temp_test_user1;
+----------+-------------+
| Field | Type |
+----------+-------------+
| id | int |
| username | varchar(20) |
| status | varchar(1) |
+----------+-------------+
# DESC temp_test_user2;
+----------+-------------+
| Field | Type |
+----------+-------------+
| id | int |
| username | varchar(20) |
| status | varchar(1) |
+----------+-------------+
尝试存入枚举选项之外的数据,不会有任何报错。数据当然也会被存入数据库中
In [14]: u1.status='X'
In [15]: u1.save()
In [16]: u2.status='X'
In [17]: u2.save()
修改后,Django后台管理页面会这样显示,不要在意用户名和上面的不一样,图片是以前的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。