Django ORM:天使与魔鬼

2022-11-02 14:04:11 浏览数 (1)

魔鬼的陷阱

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() 预期返回

代码语言:javascript复制
[
    {
        "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.2607792020-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 在数据库中做数据处理,而不用放到内存中,在数据量较大时有比较好的效果,比如:

代码语言:javascript复制
queryset = queryset.extra(select={'username': "CONCAT(username, '@', domain)"})

在模糊查询时,匹配最短结果

代码语言:javascript复制
MyModel.objects.extra(select={'myfield_length':'Length(myfield)'}).order_by('myfield_length')

但在同时需要格外小心, extra() 在参数上存在注入风险,所有可能的用户输入的 SQL 拼接,都应该交给 Django 处理。

代码语言:javascript复制
# 有注入风险, 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 行锁保证正确性。

代码语言:javascript复制
with transaction.atomic():
    # 仅在 transaction 内生效
    Entry.objects.select_for_update().filter(name="Hello")

但是同时需要注意,上锁的顺序,避免产生死锁。

0 人点赞