Django仅用id实例化生产可用User模型避免额外SQL查询
我想了解是否可以仅使用数据库记录的id实例化一个空的模型实例。我已经找到了创建测试用模拟模型的方法,但需要适用于生产环境的解决方案。
场景说明
我需要列出所有选择开启公开展示模式的用户的设置,初始写法如下:
user_displayed_list = UserPublicProfile.objects.filter( displayed = True, ).only( 'user_id', 'is_premium', ) user_settings_list = [] for user_displayed in user_displayed_list: # 需要将User实例传入后续方法 user_settings = self.get_user_settings(user_displayed.user) user_settings_list.append(user_settings) # 但访问user_displayed.user会触发新的SQL查询
我知道可以用select_related优化查询集:
user_displayed_list = UserPublicProfile.objects.filter( displayed = True, ).select_related( 'user' ).only( 'user', 'is_premium', )
但这种写法会产生不必要的表JOIN关联,因为我在get_user_settings()方法中仅需要用到user的id字段。
get_user_settings()方法示例(实际项目包含更多业务逻辑):
def get_user_settings(self, user) user_settings = UserSettings.objects.get(user = user) return user_settings
我的核心诉求:
- 实例化一个仅填充
id字段的真正的User模型对象,不接受自定义空类 - 不需要额外数据库查询、不需要JOIN关联
- 可直接传入依赖User实例的业务方法使用
期望的使用方式类似:
for user_displayed in user_displayed_list: FakeUser = User.objects.create_fake(id = user_displayed.user_id) # 将User实例传入后续方法 user_settings = self.get_user_settings(FakeUser)
Django原生支持这种用法,不需要额外依赖第三方库,直接实例化模型类传入主键即可,完全不会触发额外数据库查询,拿到的就是标准的User模型实例。
最简实现
直接手动创建User实例,只传id参数即可:
for user_displayed in user_displayed_list: # 仅传id实例化,不会访问数据库 fake_user = User(id=user_displayed.user_id) user_settings = self.get_user_settings(fake_user)
注意:这个实例是内存中的临时对象,如果你后续调用
save()方法且未补全其他必填字段,会触发数据库校验错误,仅用于外键传参查询的场景完全安全。
如果你需要避免误操作调用save,可以加上fake_user._state.adding = False,标记实例为已存在状态,就算误调用save也会走更新逻辑而非新建逻辑,但依然不建议用这个临时实例做写入操作。
实现原理
Django ORM处理外键关联查询时,只要传入的模型实例带有正确的主键id值,就会直接提取这个id做关联条件查询,不会访问实例上的其他字段,也不会触发实例的懒加载。你调用UserSettings.objects.get(user=user)时,ORM实际生成的SQL只会用到user实例的主键值,完全不会读取其他字段。
如果你后续需要访问user_displayed.user属性又不想触发额外查询,可以提前给外键缓存赋值:
for user_displayed in user_displayed_list: fake_user = User(id=user_displayed.user_id) # 提前填充外键缓存,后续访问user_displayed.user不会触发查询 user_displayed._state.fields_cache["user"] = fake_user user_settings = self.get_user_settings(fake_user)
封装成你期望的调用方法
如果要在多处复用这个逻辑,可以给User模型加个类方法,完全实现你预期的create_fake效果:
from django.db import models class User(models.Model): # 原有User模型字段 username = models.CharField(max_length=150) # ... 其他业务字段 @classmethod def create_fake(cls, id): instance = cls(id=id) instance._state.adding = False return instance
封装后就可以直接按你写的方式调用:
for user_displayed in user_displayed_list: fake_user = User.create_fake(id=user_displayed.user_id) user_settings = self.get_user_settings(fake_user)
性能对比
- 初始写法:循环N次触发N次User表查询,存在典型的N+1问题
- select_related写法:1次SQL带JOIN联查User表字段
- 本方案:仅1次查询UserPublicProfile表的SQL,无JOIN、无额外查询,性能最优
内容的提问来源于stack exchange,提问作者Samuel Dauzon

