Django+Djongo存储用户触发django.db.utils.DatabaseError问题求助
Django + Djongo 切换MongoDB后保存用户触发E11000重复键错误问题解决
问题背景
使用Django3.1.9 + djongo1.3.6,将原PostgreSQL项目切换到MongoDB配置后,保存用户时出现django.db.utils.DatabaseError,底层触发pymongo.errors.BulkWriteError,错误核心为admin_org_id字段的null值导致E11000重复键冲突。
错误日志
Internal Server Error: /signup/ Traceback (most recent call last): File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\sql2mongo\query.py", line 857, in parse return handler(self, statement) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\sql2mongo\query.py", line 929, in _insert query.execute() File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\sql2mongo\query.py", line 397, in execute res = self.db[self.left_table].insert_many(docs, ordered=False) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\pymongo\collection.py", line 770, in insert_many blk.execute(write_concern, session=session) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\pymongo\bulk.py", line 533, in execute return self.execute_command(generator, write_concern, session) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\pymongo\bulk.py", line 366, in execute_command _raise_bulk_write_error(full_result) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\pymongo\bulk.py", line 140, in _raise_bulk_write_error raise BulkWriteError(full_result) pymongo.errors.BulkWriteError: batch op errors occurred, full error: {'writeErrors': [{'index': 0, 'code': 11000, 'keyPattern': {'admin_org_id': 1}, 'keyValue': {'admin_org_id': None}, 'errmsg': 'E11000 duplicate key error collection: ssoDB.users_user index: admin_org_id_1 dup key: { admin_org_id: null }', 'op': {'password': 'pbkdf2_sha256$216000$bbg7CWLFi44A$69XZYjuc/T1Unv0f7QXrTRTURDbPT+HhT8A2AH+WE14=', 'last_login': None, 'is_superuser': False, 'id': UUID('a9a2b67b-fb63-4b55-b889-f24676949fa6'), 'email': 'testuser@vigastudios.com', 'avatar': '', 'first_name': 'Abhijit', 'last_name': None, 'nickname': None, 'phone_number': '+919083242266', 'organization_id': None, 'admin_org_id': None, 'is_active': True, 'is_staff': False, 'created_at': datetime.datetime(2022, 8, 9, 5, 9, 37, 236707), '_id': ObjectId('62f1ec11e10c9c015b52dbd6')}}], 'writeConcernErrors': [], 'nInserted': 0, 'nUpserted': 0, 'nMatched': 0, 'nModified': 0, 'nRemoved': 0, 'upserted': []} The above exception was the direct cause of the following exception: Traceback (most recent call last): File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\cursor.py", line 51, in execute self.result = Query( File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\sql2mongo\query.py", line 784, in __init__ self._query = self.parse() File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\sql2mongo\query.py", line 869, in parse raise exe from e djongo.exceptions.SQLDecodeError: Keyword: None Sub SQL: None FAILED SQL: INSERT INTO "users_user" ("password", "last_login", "is_superuser", "id", "email", "avatar", "first_name", "last_name", "nickname", "phone_number", "organization_id", "admin_org_id", "is_active", "is_staff", "created_at") VALUES (%(0)s, %(1)s, %(2)s, %(3)s, %(4)s, %(5)s, %(6)s, %(7)s, %(8)s, %(9)s, %(10)s, %(11)s, %(12)s, %(13)s, %(14)s) Params: ('pbkdf2_sha256$216000$bbg7CWLFi44A$69XZYjuc/T1Unv0f7QXrTRTURDbPT+HhT8A2AH+WE14=', None, False, UUID('a9a2b67b-fb63-4b55-b889-f24676949fa6'), 'testuser@vigastudios.com', '', 'Abhijit', None, None, '+919083242266', None, None, True, False, datetime.datetime(2022, 8, 9, 5, 9, 37, 236707)) Version: 1.3.6 The above exception was the direct cause of the following exception: Traceback (most recent call last): File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\db\backends\utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\cursor.py", line 59, in execute raise db_exe from e djongo.database.DatabaseError The above exception was the direct cause of the following exception: Traceback (most recent call last): File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\core\handlers\exception.py", line 47, in inner response = get_response(request) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\core\handlers\base.py", line 181, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view return view_func(*args, **kwargs) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\views\generic\base.py", line 70, in view return self.dispatch(request, *args, **kwargs) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\rest_framework\views.py", line 505, in dispatch response = self.handle_exception(exc) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\rest_framework\views.py", line 465, in handle_exception self.raise_uncaught_exception(exc) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\rest_framework\views.py", line 476, in raise_uncaught_exception raise exc File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\rest_framework\views.py", line 502, in dispatch response = handler(request, *args, **kwargs) File "C:\Users\webde\Work\Fintract\fraudify_sso\users\views.py", line 78, in post User.objects.create_user(first_name="Abhijit", return self._execute_with_wrappers(sql, params, many=False, executor=self._execute) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\db\backends\utils.py", line 75, in _execute_with_wrappers return executor(sql, params, many, context) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\db\backends\utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\db\utils.py", line 90, in __exit__ raise dj_exc_value.with_traceback(traceback) from exc_value File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\django\db\backends\utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "C:\Users\webde\Work\Fintract\fraudify_sso\venv\lib\site-packages\djongo\cursor.py", line 59, in execute raise db_exe from e django.db.utils.DatabaseError
模型代码
import uuid from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import PermissionsMixin from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from rest_framework.exceptions import APIException from .managers import CustomUserManager email_superuser = 'superuser@vigastudios.com' class Organization(models.Model): name = models.CharField(max_length=50) joining_date = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name class User(AbstractBaseUser, PermissionsMixin): """ Model to store all kinds of users in the database. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, serialize=False, verbose_name='ID') email = models.EmailField(_('email address'), unique=True) avatar = models.ImageField(upload_to='static', null=True, blank=True) first_name = models.CharField(_('first name'), max_length=30) last_name = models.CharField( _('last name'), max_length=30, blank=True, null=True) nickname = models.CharField( _('nickname'), max_length=30, blank=True, null=True) phone_number = PhoneNumberField() organization = models.ForeignKey(Organization, on_delete=models.SET_NULL, related_name='users', null=True, blank=True) admin_org = models.OneToOneField(Organization, on_delete=models.SET_NULL, related_name='admin', null=True, blank=True) is_active = models.BooleanField(_('is active'), default=True) is_staff = models.BooleanField(_('staff'), default=False) created_at = models.DateTimeField(auto_now_add=True) objects = CustomUserManager() EMAIL_FIELD = 'email' USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['first_name', 'phone_number'] class Meta: verbose_name = _('user') verbose_name_plural = _('users') def __str__(self): user_representation = self.first_name if self.last_name: user_representation += f" {self.last_name}" return user_representation def save(self, force_insert=False, force_update=False, using=None, update_fields=None): try: self.clean() except ValidationError as e: raise APIException(str(e)) if self.admin_org: if self.organization: if self.organization != self.admin_org: raise APIException( str(f'User is a part of {self.organization}')) else: self.organization = self.admin_org super(User, self).save() class OrganizationJoinRequest(models.Model): org = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='join_requests') user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='org_requests') class Meta: unique_together = ['org', 'user'] def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if self.user.organization == self.org: raise APIException(f'User is already a member of {self.org}') super().save(force_insert=False, force_update=False, using=None, update_fields=None) def accept(self): self.user.organization = self.org self.user.save() self.delete()
问题原因
MongoDB和PostgreSQL对唯一索引下的null值处理逻辑不同:
- PostgreSQL中,唯一约束允许多个null值,因为null不被视为相等的取值
- MongoDB中,唯一索引会把所有null值判定为相同的键,当第二个用户的
admin_org_id为null时,就会触发E11000重复键错误
解决方案
方案1:移除MongoDB中admin_org_id的唯一索引(推荐)
利用模型中已有的业务逻辑保证唯一性,不需要依赖数据库索引:
- 进入MongoDB Shell,切换到目标数据库:
use ssoDB
- 查看集合索引,确认
admin_org_id_1索引存在:
db.users_user.getIndexes()
- 删除该唯一索引:
db.users_user.dropIndex("admin_org_id_1")
原模型的save方法已经包含校验逻辑,确保一个Organization只能有一个admin,业务层面的唯一性可以得到保障。
方案2:使用MongoDB稀疏索引
稀疏索引仅包含有非null值的文档,不会将null值纳入索引,从而避免重复键错误:
db.users_user.createIndex( { admin_org_id: 1 }, { unique: true, sparse: true } )
注意:稀疏索引可能影响查询性能,需结合业务场景评估后使用。
方案3:修改模型字段类型(不推荐)
将admin_org的OneToOneField改为ForeignKey,但这会破坏原本的一对一关联逻辑,仅适用于业务上允许多个用户关联同一Organization作为admin的场景。
内容的提问来源于stack exchange,提问作者χerфmiи
相关产品推荐
相关产品推荐

