You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的唯一索引(推荐)

利用模型中已有的业务逻辑保证唯一性,不需要依赖数据库索引:

  1. 进入MongoDB Shell,切换到目标数据库:
use ssoDB
  1. 查看集合索引,确认admin_org_id_1索引存在:
db.users_user.getIndexes()
  1. 删除该唯一索引:
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и

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 19:39:26