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

Django配置ManyToManyField后新增UserSearchLocation报user_id不存在错误

问题根因

column base_usersearchlocation.user_id does not exist报错的核心原因是模型关联逻辑完全错位:

  • 定义的UserSearchLocation表仅包含city_name字段,没有任何和User的外键关联,但Django admin渲染该模型操作逻辑时,会尝试从这张表查询不存在的user_id字段,本质是多对多中间表、搜索记录表、城市基础信息表的职责完全混淆。
  • City模型已经通过through='UsersLocations'指定和User的多对多中间表为UsersLocations,又单独定义UserSearchLocation模型,既不关联用户也不关联城市,完全没有接入多对多关联链路,属于冗余定义。
  • 结合「存储用户搜索过的城市、登录用户仅可查看自己搜索过的城市」的业务需求,根本不需要单独的UserSearchLocation模型,现有UsersLocations中间表完全可以承担搜索记录存储职责,重复建表只会导致关联逻辑混乱。
修正步骤

1. 清理冗余模型,修正关联定义

直接删除无用的UserSearchLocation模型,把搜索时间这类扩展字段加到中间表UsersLocations上即可,修正后的模型代码如下:

from django.db import models
from django.contrib.auth.models import User

class City(models.Model):
    id = models.AutoField(primary_key=True, editable=False)
    location = models.CharField(max_length=85)
    country = models.CharField(max_length=85, blank=True)
    country_code = models.CharField(max_length=2, blank=True)
    latitude = models.DecimalField(max_digits=6, decimal_places=4,
                                   null=True, blank=True)
    longitude = models.DecimalField(max_digits=6, decimal_places=4,
                                    null=True, blank=True)
    zip_code = models.PositiveIntegerField(default=0)
    # 多对多字段放到普通字段后定义,避免引用顺序问题
    user_searched_locations = models.ManyToManyField(
        User, 
        through='UsersLocations',
        through_fields=('city', 'user'),
        related_name="my_cities",
        blank=True
    )

    def __str__(self):
        return f'{self.location}, {self.country_code}'

    def save(self, *args, **kwargs):
        self.location = self.location.capitalize()
        self.country = self.country.capitalize()
        self.country_code = self.country_code.capitalize()
        return super(City, self).save(*args, **kwargs)
    
    class Meta: 
        verbose_name_plural = 'cities'
        unique_together = ("location", "country_code")


class UsersLocations(models.Model):
    id = models.AutoField(primary_key=True, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
    city = models.ForeignKey(
        City,
        on_delete=models.CASCADE,
        related_name='locations_by_users',
        null=False
    )
    # 可新增搜索时间字段,方便后续做搜索历史排序
    search_time = models.DateTimeField(auto_now_add=True)

    class Meta:
        # 增加唯一约束,避免同一用户重复绑定同一城市产生冗余数据
        unique_together = ('user', 'city')

注意:多对多字段不要放在模型主键前面定义,容易触发模型引用顺序问题;业务要求必须关联用户和城市的场景下,外键不要设null=True,避免产生无归属的脏数据。

2. 执行数据库迁移

因为删除了冗余模型、调整了字段约束,必须重新生成迁移文件并同步到数据库:

python manage.py makemigrations
python manage.py migrate

如果迁移时提示旧的UserSearchLocation表存在冲突,先备份该表数据后删除对应旧迁移记录,再重新执行迁移即可。

3. 实现数据权限控制逻辑

不需要在模型层做特殊改造,直接在API视图层过滤查询集即可。如果使用DRF框架,重写视图的get_queryset方法就能实现「用户只能看到自己搜索过的城市」的要求:

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
# 引入自己定义的序列化器和模型
from .serializers import CitySerializer
from .models import City

class CityListView(generics.ListAPIView):
    serializer_class = CitySerializer
    # JWT认证会自动将当前登录用户注入request.user
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return City.objects.filter(locations_by_users__user=self.request.user)

用户触发城市搜索时,按以下逻辑写入数据即可:

# 先查询城市是否存在,不存在则创建基础城市数据
city, created = City.objects.get_or_create(
    location=search_location_name.capitalize(),
    country_code=search_country_code.upper(),
    defaults={
        "country": search_country_name.capitalize(),
        # 经纬度、邮编等字段从第三方位置接口返回值填充
    }
)
# 绑定当前用户和城市的关联,加ignore_conflicts避免重复插入报错
UsersLocations.objects.get_or_create(user=request.user, city=city)

该逻辑下,其他用户搜索同一个城市时,只会新增自己和该城市的关联记录,不会修改城市基础数据,同时每个用户只能查询到自己关联过的城市,完全匹配业务要求。

报错补充说明

如果清理冗余模型后仍然触发同类错误,大概率是之前给UserSearchLocation加过指向User的外键,后续删除字段时没有执行数据库迁移,旧表结构残留或admin缓存逻辑仍在尝试读取不存在的user_id字段,清空冗余表重新迁移后问题会完全消失。

内容的提问来源于stack exchange,提问作者vhij

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 12:03:26