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

如何在Django中避免使用ManyToMany字段?附模型示例

替代Django ManyToMany字段的解决方案

问题背景

原代码中Account模型通过ManyToManyField关联Role模型,但由于该字段是Django专属特性,需要替换为原生数据库支持的方案,且需完全保留原功能。

实现思路

Django的ManyToManyField底层本质是手动创建的关联中间表,因此我们直接模拟这一机制:

  • 手动创建关联中间表,用两个ForeignKey分别关联Account和Role,并设置联合唯一约束避免重复关联
  • 在Account模型中封装自定义方法,模拟原ManyToManyField的核心操作(如all()、add()、remove())
  • 适配原有业务逻辑(如verify_user_type方法)

修改后的完整代码

from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from django.core.validators import RegexValidator
import uuid
from django.utils import timezone


ADMIN = 0
CUSTOMER = 1
SELLER = 2
DELIVERY_PARTNER = 4

class Role(models.Model):
    '''
    角色条目由系统维护,通过Django数据迁移自动创建。
    '''
    ROLE_CHOICES = (
        (ADMIN, 'admin'),
        (CUSTOMER, 'customer'),
        (SELLER, 'seller'),
        (DELIVERY_PARTNER, 'delivery'),
    )

    id = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, primary_key=True)

    def __str__(self):
        return self.get_id_display()

# 手动创建关联中间表,模拟ManyToMany的底层实现
class AccountRole(models.Model):
    account = models.ForeignKey('Account', on_delete=models.CASCADE, related_name='account_roles')
    role = models.ForeignKey(Role, on_delete=models.CASCADE)

    class Meta:
        # 联合唯一约束:同一个用户不能重复关联同一个角色
        unique_together = ('account', 'role')

class UserManager(BaseUserManager):
    '''
    自定义用户模型的管理器
    '''
    def create_user(self, mobile_number, password=None):
        """
        创建并返回一个带有手机号和密码的用户
        """
        if not mobile_number:
            raise ValueError('用户必须提供手机号码')

        user = self.model(
            mobile_number=mobile_number
        )
        user.set_password(password)
        user.save(using=self._db)
        # 添加默认角色:CUSTOMER
        default_role = Role.objects.get(id=CUSTOMER)
        user.add_role(default_role)
        return user

    def create_superuser(self, mobile_number, password):
        """
        创建并返回拥有超级用户权限的用户
        """
        if password is None:
            raise TypeError('超级用户必须设置密码')

        user = self.create_user(mobile_number, password)
        user.is_superuser = True
        user.is_staff = True
        # 添加ADMIN角色
        admin_role = Role.objects.get(id=ADMIN)
        user.add_role(admin_role)
        user.save()
        return user

class Account(AbstractBaseUser):
    '''存储手机号和认证信息的用户模型'''
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(
        verbose_name='邮箱地址',
        max_length=255,
        null=True,
    )
    phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$', message="手机号格式必须为:'+999999999',最多允许15位数字。")
    mobile_number = models.CharField(validators=[phone_regex], max_length=17, blank=False, null=False, unique=True) 

    otp = models.CharField(max_length=6, null=True, unique=True)
    user_registered_on = models.DateTimeField(default=timezone.now, blank=True)

    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    USERNAME_FIELD = 'mobile_number'
    REQUIRED_FIELDS = []

    objects = UserManager()

    def get_full_name(self):
        pass

    def get_short_name(self):
        pass

    @property
    def is_staff(self):
       return self.is_superuser

    def has_perm(self, perm, obj=None):
       return self.is_superuser

    def has_module_perms(self, app_label):
       return self.is_superuser

    @is_staff.setter
    def is_staff(self, value):
        self._is_staff = value

    def __str__(self):
        return self.mobile_number
    
    # -------------------------- 模拟ManyToManyField的核心方法 --------------------------
    @property
    def type(self):
        """模拟原ManyToManyField的all()方法,返回当前用户的所有角色"""
        return Role.objects.filter(id__in=self.account_roles.values('role_id'))

    def add_role(self, role):
        """模拟原ManyToManyField的add()方法,给用户添加角色"""
        if not isinstance(role, Role):
            # 支持传入角色ID或Role对象
            role = Role.objects.get(id=role)
        AccountRole.objects.get_or_create(account=self, role=role)

    def remove_role(self, role):
        """模拟原ManyToManyField的remove()方法,移除用户的指定角色"""
        if not isinstance(role, Role):
            role = Role.objects.get(id=role)
        AccountRole.objects.filter(account=self, role=role).delete()

    def verify_user_type(self, role_id):
        """验证用户是否拥有指定角色,优化原逻辑"""
        return self.account_roles.filter(role_id=role_id).exists()

    class Meta:
        db_table = "login"

关键修改说明

  • 新增AccountRole中间表:用两个ForeignKey分别关联Account和Role,unique_together约束确保用户不会重复关联同一角色,和Django自动生成的中间表行为完全一致。
  • 替换ManyToManyField:移除原type = models.ManyToManyField(Role, default=[1]),通过@property定义type属性模拟原all()方法,同时添加add_role、remove_role方法模拟原字段的添加/移除操作。
  • 处理默认角色:在UserManager的create_user方法中手动添加默认的CUSTOMER角色,替代原ManyToManyField的default参数。
  • 优化verify_user_type方法:直接通过中间表查询,比原循环遍历更高效。
  • 修正错误:移除原create_user中错误的normalize_email(mobile_number)调用,因为手机号不是邮箱格式。

内容的提问来源于stack exchange,提问作者Pankaj Singh Rana

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 11:15:40