Django REST Framework创建关联字段记录时遇非空约束错误求助
这是个新手问题,我在StackOverflow没找到足够匹配的答案,所以来提问。我在创建含外键关联已有记录的模型实例时遇到困难,不清楚代码问题出在哪。
我的应用里有Company和BalanceSheet两个模型,是一对多关系:
模型代码
class Company(models.Model): cik = models.IntegerField(default=0, unique=True) symbol = models.CharField(max_length=4, unique=True) name = models.CharField(max_length=255, unique=True) def __str__(self): return self.symbol class BalanceSheet(models.Model): company = models.ForeignKey(Company, null=True, on_delete=models.CASCADE, related_name='balance_sheets',) date = models.DateField() profit = models.BigIntegerField() loss = models.BigIntegerField() class Meta: unique_together = (('company', 'date'),) def __str__(self): return '%s - %s' % (self.company, self.date)
序列化器代码
class BalanceSheetSerializer(serializers.ModelSerializer): company = serializers.StringRelatedField() class Meta: model = BalanceSheet fields = ('company','date','profit','loss') class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company fields = ('cik', 'symbol', 'name')
视图代码
class BalanceSheetCreate(generics.CreateAPIView): model = BalanceSheet queryset = BalanceSheet.objects.all() serializer_class = BalanceSheetSerializer
URL配置
url(r'^(?P<symbol>[A-Z]{1,4})/create-balance-sheet/$', views.BalanceSheetCreate.as_view(), name='create_balance_sheet'),
目前读取数据没问题,但创建记录时出错:
执行curl命令:
curl http://localhost:8000/financials/AAPL/create-balance-sheet/ -X POST -d "company=AAPL&date=1968-04-17&profit=1&loss=1"
报错信息:
IntegrityError at /financials/AAPL/create-balance-sheet/ null value in column "company_id" violates not-null constraint
即使去掉POST数据里的company参数,还是报相同错误。我以为已经通过URL和POST数据指定了关联公司,请问该怎么解决这个错误?
环境:Python3.6、Django1.11、Django REST Framework3.7.7
我来帮你分析下问题所在,核心问题出在你的BalanceSheetSerializer里用的StringRelatedField上——这个字段是只读的!它只会把外键对应的__str__值返回给前端,完全不处理前端传过来的用于创建关联的数据,所以DRF根本没把你传的company=AAPL转换成对应的Company实例,最终导致company_id为空,触发了数据库的非空约束错误。
下面给你两种可行的解决办法,你可以根据自己的需求选择:
方法一:修改序列化器,支持通过symbol关联Company
把StringRelatedField换成SlugRelatedField,这个字段可以通过指定的slug字段(这里就是symbol)来关联已有的Company实例,同时支持读写操作。修改后的序列化器代码如下:
class BalanceSheetSerializer(serializers.ModelSerializer): company = serializers.SlugRelatedField( queryset=Company.objects.all(), slug_field='symbol' # 指定用Company的symbol字段来匹配 ) class Meta: model = BalanceSheet fields = ('company','date','profit','loss')
修改后,你之前的curl命令就能正常工作了,DRF会自动根据你传的company=AAPL找到对应的Company实例,然后关联到新创建的BalanceSheet上。
方法二:从URL参数自动获取Company,无需前端传company参数
既然你的URL里已经包含了symbol参数(比如AAPL),其实可以直接从URL里拿到这个值,然后在视图里自动设置关联的Company,这样前端请求时就不需要再传company参数了,既简化了请求,也避免了前端传错的可能。
修改你的视图代码,重写perform_create方法:
class BalanceSheetCreate(generics.CreateAPIView): model = BalanceSheet queryset = BalanceSheet.objects.all() serializer_class = BalanceSheetSerializer def perform_create(self, serializer): # 从URL参数里获取symbol,找到对应的Company实例 symbol = self.kwargs.get('symbol') company = Company.objects.get(symbol=symbol) # 保存时传入company参数 serializer.save(company=company)
同时,你需要把序列化器里的company字段改回默认的外键处理(或者设置为只读,因为不需要前端传了):
class BalanceSheetSerializer(serializers.ModelSerializer): # 如果需要返回company的信息,可以保留StringRelatedField,但要加上read_only=True company = serializers.StringRelatedField(read_only=True) class Meta: model = BalanceSheet fields = ('company','date','profit','loss')
这时候你的curl命令就可以去掉company=AAPL参数了:
curl http://localhost:8000/financials/AAPL/create-balance-sheet/ -X POST -d "date=1968-04-17&profit=1&loss=1"
小提示
第二种方法其实更推荐,因为URL已经明确指定了要关联的公司,前端不需要重复传递,而且能保证创建的BalanceSheet一定属于URL里指定的Company,避免出现前后端传递不一致的问题。
内容的提问来源于stack exchange,提问作者jsamsf

