作为一只以 Django 作为主力开发框架的 CRUD Boy ,时常和它的 ORM 缠绵悱恻、纠缠不清,特此记录一下这些笑与泪的记忆。
QuerySet
的类型 objects.values()
返回的并不是简单类型的数据,而是 QuerySet
。一般直接用来做 Response
没有问题,但是要知道 QuerySet
是不能被 pickle
的,如果使用到 Django Cache
之类功能,直接用 values()
当作返回会死得很惨。
很多时候我们需要限制 QuerySet
返回的字段以加快 DB 查询的速度(比如一些没索引的长字段),这时候可能的两个方法: only()
& values()
。
但实际情况是,使用 values()
会改变 queryset._iterable_class
,如果后面还有更多的级联查询,会导致最后的结果为 Dict
而不是 QuerySet
。
values()
存在一个模型
class Foo(models.Model):
name = models.CharField(**some_params)
bars = models.ManyToManyField(**some_params)
存在一条记录
foo:
name: tom
bars:
- a
- b
values()
预期返回
[
{
"name": "tom",
"bars": ["a", "b"]
}
]
实际返回
[
{
"name": "tom",
"bars": "a"
},
{
"name": "tom",
"bars": "b"
}
]
没有什么太好的调整办法,只能注意 + 规避,详见:
QuerySet API reference | Django documentation | Django
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#values
我们要时刻记住, orm
只是做一个映射,有时候拿到的对象和我们预想并不能完全一致。
class Foo(models.Model):
created = models.DateTimeField()
# 这里先忽略 timezone 问题
f1 = Foo(created='2020-09-18 09:46:23.544799')
# 字符串会被存储,Django 做了隐式转换
f1.save()
# str
print(type(f1.created))
f2 = Foo.objects.get(pk=f1.pk)
# Datetime 对象!
print(type(f2.created))
通过以上的例子就能知道,我们自己创建的内存对象 f1
和通过 orm
拿出来的内存对象 f2
完全不是同一个东西,虽然他们都可以操作同一条数据库记录,但如果在内存对象里做比较就会有很多问题,比如下面的例子
class Foo(models.Model):
created = models.DateTimeField(auto_now_add=True)
# 假定 Foo 表中已经存在了比较多的记录
f = Foo.objects.create()
# 我们预期是获取按照时间来排序,f 的前一条记录
o = Foo.objects.filter(created_lt=f.created).latest('created')
assert o.pk == f.pk
# mysql 版本大于 5.6.4 时 -> False
# mysql 版本小于 5.6.4 时 -> True
原因很简单,当 mysql
版本小于 5.6.4
时是不支持 microseconds
的,由于我们的 f
是内存对象,拿到的 created
又是有 microseconds
的,相当于我们在用 2020-09-18 09:24:38.260779
和 2020-09-18 09:24:38.000000
做比较, o
一直拿到的就是 f
对应的记录...
.query
我们常常用 queryset.query
去检查复杂的查询语句,但实际上 query
属性并不能真实反应提交到 DB 中的 sql
,可以参考如下链接:
QuerySet.query.__str__() does not generate valid MySQL query with dates
https://code.djangoproject.com/ticket/17741
那么如何调试提交到 DB 中的具体语句呢?
from django.db import connection
# 在语句提交之后,立即打印
# 同时需要记得开启 DEBUG = True
print(connection.queries)
再或者,直接在 DB 中开启 general_log 。
QuerySet API reference | Django documentation | Django
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#extra
extra()
可以利用 sql
在数据库中做数据处理,而不用放到内存中,在数据量较大时有比较好的效果,比如:
queryset = queryset.extra(select={'username': "CONCAT(username, '@', domain)"})
在模糊查询时,匹配最短结果
MyModel.objects.extra(select={'myfield_length':'Length(myfield)'}).order_by('myfield_length')
但在同时需要格外小心, extra()
在参数上存在注入风险,所有可能的用户输入的 SQL 拼接,都应该交给 Django 处理。
# 有注入风险, username 不会被转义,可以直接注入
Entry.objects.extra(where=[f"headline='{username}'"])
# 安全,Django 会将 username 内容转义
Entry.objects.extra(where=['headline=%s'], params=[username])
JsonField
的福音—— JSON_SEARCH
有时候我们需要使用动态字段,并且保证动态字段的值全表唯一。动态字段我们使用 LONGTEXT
存储,格式为 JSON
。如果手动处理,需要将整个表的字段放到内存,并做唯一校验,非常麻烦且耗时。
所以还是一个道理,把这个逻辑交给 DB
select * from profiles_profile where JSON_SEARCH(extras, "one", "aaa") is not null;
多个操作互斥的情况下,可以使用 select_for_update
行锁保证正确性。
with transaction.atomic():
# 仅在 transaction 内生效
Entry.objects.select_for_update().filter(name="Hello")
但是同时需要注意,上锁的顺序,避免产生死锁。