👹 Django ORM:天使与魔鬼

date
Nov 27, 2020
slug
django-orm-best-practice.html
status
Published
tags
tech
django
orm
best-practice
web
summary
平时是小天使,出了问题就是大撒旦
type
Post
 
 
作为一只以 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"
    }
]
 
没有什么太好的调整办法,只能注意 + 规避,详见:
notion image
 

ORM 终究只是 ORM

 
我们要时刻记住, 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 完全不是同一个东西,虽然他们都可以操作同一条数据库记录,但如果在内存对象里做比较就会有很多问题,比如下面的例子
 

Mysql 低版本时间精度问题

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.2607792020-09-18 09:24:38.000000 做比较, o 一直拿到的就是 f 对应的记录...
notion image
 

虚假的 .query

 
我们常常用 queryset.query 去检查复杂的查询语句,但实际上 query 属性并不能真实反应提交到 DB 中的 sql ,可以参考如下链接:
 
那么如何调试提交到 DB 中的具体语句呢?
from django.db import connection

# 在语句提交之后,立即打印
# 同时需要记得开启 DEBUG = True
print(connection.queries)
再或者,直接在 DB 中开启 general_log
 

天使的眼泪

巧用 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")
 
但是同时需要注意,上锁的顺序,避免产生死锁。

© bluesyu 2019 - 2024

powered by nobelium