如何在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
相关产品推荐
相关产品推荐

