魔鬼的陷阱
QuerySet 的类型
有时候希望它简单一点
有时候希望它坚持自我
多对多和 values()
ORM 终究只是 ORM
隐式转换
Mysql 低版本时间精度问题
虚假的 .query
天使的眼泪
巧用 extra
JsonField 的福音—— JSON_SEARCH
行锁的支持
作为一只以 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()
存在一个模型
代码语言:javascript复制class Foo(models.Model):
name = models.CharField(**some_params)
bars = models.ManyToManyField(**some_params)
存在一条记录
代码语言:javascript复制foo:
name: tom
bars:
- a
- b
values()
预期返回
[
{
"name": "tom",
"bars": ["a", "b"]
}
]
实际返回
代码语言:javascript复制[
{
"name": "tom",
"bars": "a"
},
{
"name": "tom",
"bars": "b"
}
]
没有什么太好的调整办法,只能注意 规避,详见:
QuerySet API reference | Django documentation | Django
Django provides a range of refinement methods that modify either the types of results returned by the or the way its SQL query is executed. Returns a new containing objects that do not match the given lookup parameters. The lookup parameters () should be in the format described in Field lookups below.
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#values
ORM 终究只是 ORM
我们要时刻记住, orm
只是做一个映射,有时候拿到的对象和我们预想并不能完全一致。
隐式转换
代码语言:javascript复制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
完全不是同一个东西,虽然他们都可以操作同一条数据库记录,但如果在内存对象里做比较就会有很多问题,比如下面的例子
Mysql 低版本时间精度问题
代码语言:javascript复制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
If you have a date parameter in a QuerySet, the string of the query is not valid SQL in MySQL and will generate a warning.
https://code.djangoproject.com/ticket/17741
那么如何调试提交到 DB 中的具体语句呢?
代码语言:javascript复制from django.db import connection
# 在语句提交之后,立即打印
# 同时需要记得开启 DEBUG = True
print(connection.queries)
再或者,直接在 DB 中开启 general_log 。
天使的眼泪
巧用 extra
QuerySet API reference | Django documentation | Django
Django provides a range of refinement methods that modify either the types of results returned by the or the way its SQL query is executed. Returns a new containing objects that do not match the given lookup parameters. The lookup parameters () should be in the format described in Field lookups below.
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#extra
extra()
可以利用 sql
在数据库中做数据处理,而不用放到内存中,在数据量较大时有比较好的效果,比如:
queryset = queryset.extra(select={'username': "CONCAT(username, '@', domain)"})
在模糊查询时,匹配最短结果
代码语言:javascript复制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
代码语言:javascript复制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")
但是同时需要注意,上锁的顺序,避免产生死锁。