问题:在后台管理界面添加条目时,报外键冲突导致添加失败。
bash
django.db.utils.IntegrityError: (1452, 'Cannot add or update a child row: a foreign key constraint fails (`blog`.`django_admin_log`, CONSTRAINT `django_admin_log_user_id_c564eba6_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`))')
auth_user 表: 新的超级用户条目将被添加到该表中,包含用户名、哈希密码、电子邮件等。
django_admin_log 表: 在某些情况下,Django Admin 会记录管理员操作。如果 createsuperuser 命令使用 Django Admin 的某些组件,则可能会创建日志记录。
当继承自 AbstractUser
,并自定义一些字段(如 mobile
),Django 将根据这个自定义模型来创建和管理用户。自定义用户模型对应于 tb_user
表,而不是默认的 auth_user
表。因此,执行 createsuperuser
时,数据会写入 tb_user
,当然前提是 AUTH_USER_MODEL
配置正确。
查找原因
查了django_admin_log表,其FOREIGN KEY (user_id
)是依赖于auth_user表的id:
sql
mysql> SHOW CREATE TABLE django_admin_log;
| django_admin_log | CREATE TABLE `django_admin_log` (
`id` int NOT NULL AUTO_INCREMENT,
`action_time` datetime(6) NOT NULL,
`object_id` longtext,
`object_repr` varchar(200) NOT NULL,
`action_flag` smallint unsigned NOT NULL,
`change_message` longtext NOT NULL,
`content_type_id` int DEFAULT NULL,
`user_id` int NOT NULL,
PRIMARY KEY (`id`),
KEY `django_admin_log_content_type_id_c4bce8eb_fk_django_co` (`content_type_id`),
KEY `django_admin_log_user_id_c564eba6_fk_auth_user_id` (`user_id`),
CONSTRAINT `django_admin_log_content_type_id_c4bce8eb_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`),
CONSTRAINT `django_admin_log_user_id_c564eba6_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
CONSTRAINT `django_admin_log_chk_1` CHECK ((`action_flag` >= 0))
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 |
1 row in set (0.00 sec)
但是由于自定义了User模型,并继承于AbstractUser,并且在声明AUTH_USER_MODEL的情况下,也就是说现在根本不会用到auth_user这个表,而是自定义的tb_user表。
原因终于浮出水面,只需要将外键依赖指向自定义tb_user表即可,但是为什么没有自己引用到呢(是个好问题), Django 可能没有正确更新对用户模型的引用。
下面来尝试修复这个问题:
删除外键(有需要请先备份):
sql
mysql> ALTER TABLE django_admin_log DROP FOREIGN KEY django_admin_log_user_id_c564eba6_fk_auth_user_id;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
添加外键失败:
sql
mysql> ALTER TABLE django_admin_log
-> ADD FOREIGN KEY (user_id) REFERENCES tb_user(id);
ERROR 3780 (HY000): Referencing column 'user_id' and referenced column 'id' in foreign key constraint 'django_admin_log_ibfk_1' are incompatible.
# 外键不兼容,看看auth_user表和tb_user表有什么差异
查到auth_user的类型是int,tb_user表的id是bigint,想必这就是django没有被正确引用的原因吧。那你告诉继承的作用是什么?类型不用继承的?还是id不用继承?还是其本身有什么bug?
对比两个表
sql
mysql> desc auth_user;
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| password | varchar(128) | NO | | NULL | |
| last_login | datetime(6) | YES | | NULL | |
| is_superuser | tinyint(1) | NO | | NULL | |
| username | varchar(150) | NO | UNI | NULL | |
| first_name | varchar(150) | NO | | NULL | |
| last_name | varchar(150) | NO | | NULL | |
| email | varchar(254) | NO | | NULL | |
| is_staff | tinyint(1) | NO | | NULL | |
| is_active | tinyint(1) | NO | | NULL | |
| date_joined | datetime(6) | NO | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
11 rows in set (0.01 sec)
mysql> desc tb_user;
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| password | varchar(128) | NO | | NULL | |
| last_login | datetime(6) | YES | | NULL | |
| is_superuser | tinyint(1) | NO | | NULL | |
| username | varchar(150) | NO | UNI | NULL | |
| first_name | varchar(150) | NO | | NULL | |
| last_name | varchar(150) | NO | | NULL | |
| email | varchar(254) | NO | | NULL | |
| is_staff | tinyint(1) | NO | | NULL | |
| is_active | tinyint(1) | NO | | NULL | |
| date_joined | datetime(6) | NO | | NULL | |
| mobile | varchar(20) | NO | UNI | NULL | |
| avatar | varchar(100) | NO | | NULL | |
| user_desc | longtext | NO | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
14 rows in set (0.01 sec)
# 果然id类型不一致
科普一下:
int: 占用 4 字节的存储空间,表示的范围是 -2,147,483,648
到 2,147,483,647
bigint: 占用 8 字节的存储空间,表示的范围是 -9,223,372,036,854,775,808
到 9,223,372,036,854,775,807
。
bigint确实用不到,直接改为int,超过20亿用户再说吧。
sql
mysql> ALTER TABLE tb_user MODIFY id INT;
ERROR 3780 (HY000): Referencing column 'user_id' and referenced column 'id' in foreign key constraint 'tb_user_groups_user_id_162ae03c_fk_tb_user_id' are incompatible.
# 很显然,这个表不能轻易动,改django_admin_log表的user_id更好,因为经过排查这个表没有其他依赖
mysql> ALTER TABLE django_admin_log MODIFY user_id BIGINT;
Query OK, 0 rows affected (0.10 sec)
Records: 0 Duplicates: 0 Warnings: 0
再次新增外键:
sql
mysql> ALTER TABLE django_admin_log
-> ADD FOREIGN KEY (user_id) REFERENCES tb_user(id);
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 成功了
新的问题
接着,报了一个新的错误:
bash
django.db.utils.OperationalError: (1364, "Field 'id' doesn't have a default value")
好的,不是同一个问题,终于可以松一口气了。
另外,分享一个查询外键的方法:
sql
mysql> SELECT CONSTRAINT_NAME,TABLE_NAME
-> FROM information_schema.TABLE_CONSTRAINTS
-> WHERE TABLE_NAME = 'django_admin_log' AND CONSTRAINT_TYPE = 'FOREIGN KEY';
+--------------------------------------------------------+------------------+
| CONSTRAINT_NAME | TABLE_NAME |
+--------------------------------------------------------+------------------+
| django_admin_log_content_type_id_c4bce8eb_fk_django_co | django_admin_log |
| django_admin_log_ibfk_1 | django_admin_log |
+--------------------------------------------------------+------------------+
2 rows in set (0.01 sec)
解决新问题
sql
# 喝口水,接着看新增的问题
mysql> desc django_admin_log;
+-----------------+-------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+-------------------+------+-----+---------+-------+
| id | int | NO | PRI | NULL | |
| action_time | datetime(6) | NO | | NULL | |
| object_id | longtext | YES | | NULL | |
| object_repr | varchar(200) | NO | | NULL | |
| action_flag | smallint unsigned | NO | | NULL | |
| change_message | longtext | NO | | NULL | |
| content_type_id | int | YES | MUL | NULL | |
| user_id | bigint | YES | MUL | NULL | |
+-----------------+-------------------+------+-----+---------+-------+
8 rows in set (0.01 sec)
# 应该就是这里的问题了,竟然默认不是自增的,无语
mysql> ALTER TABLE django_admin_log
-> MODIFY COLUMN id INT AUTO_INCREMENT;
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 终于搞定
总结一下:
访问Django Admin,添加条目时报外键冲突的问题,经过查实是User模型迁移时id的类型变为bigint导致(auth_user是int),具体原因的话没还没彻底搞清楚,所以前端界面的操作记录在写django_admin_log表时,由于发生外键冲突而无法保存,因为之后的开发中依赖tb_user又新建了许多表,权衡之后修改tb_user已经变得不经济了,所以这里为了django_admin_log的user_id与tb_user的id保持一致,最终选择直接改了django_admin_log的user_id的类型,原因就是没有谁依赖它。
另一个坑就是django_admin_log在默认建表时没有设置主键的自增,导致出现第二个问题。