Django Admin报错“外键冲突”排查

发布于:2024-05-07 ⋅ 阅读:(34) ⋅ 点赞:(0)

问题:在后台管理界面添加条目时,报外键冲突导致添加失败。

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:

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 可能没有正确更新对用户模型的引用。

下面来尝试修复这个问题:

删除外键(有需要请先备份):

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
添加外键失败:
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?
对比两个表
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,6482,147,483,647

​ bigint: 占用 8 字节的存储空间,表示的范围是 -9,223,372,036,854,775,8089,223,372,036,854,775,807

bigint确实用不到,直接改为int,超过20亿用户再说吧。

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

再次新增外键:

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

# 成功了

新的问题

接着,报了一个新的错误:

django.db.utils.OperationalError: (1364, "Field 'id' doesn't have a default value")

好的,不是同一个问题,终于可以松一口气了。

另外,分享一个查询外键的方法:

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)

解决新问题

# 喝口水,接着看新增的问题

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在默认建表时没有设置主键的自增,导致出现第二个问题。