Django QuerySet简单梳理

简单的梳理一下Django中QuerySet方法,数据库中的锁机制坑后续填上

models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Category(models.Model):
name = models.CharField(max_length=200)
desc = models.TextField()

created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

class Product(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=100)
price = models.FloatField()
desc = models.TextField()

created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

1. create(**kwargs)

1
2
3
4
5
6
7
8
9
10
11
12
13
category = Category.objects.get(id=1)
p = Product.objects.create(name="apple", price=55.3, desc="", category=category)
p = Product.objects.create(name="apple", price=55.3, desc="", category_id=1)
or
defaults = {
"name": "apple",
"price": "55.3"
"desc": "",
"category": category
or
"category_id": 1
}
p = Person.objects.create(**defaults)

使用create()方法创建实例时,若model中含有外键,则外键必须添加。否则,会抛出异常:
IntegrityError: (1048, "Column 'category_id' cannot be null")
而且,当使用外键_id的形式创建时,传入关联数据的id;若直接使用外键的形式,则需要传入一个实例。

2. get_or_create(defaults=None, **kwargs)

product, created = Product.objects.get_or_create(name="apple", price=55.3, defaults={})

  • created为布尔值,当创建时返回为True
  • 若get()方法返回多条数据,该条语句同样会抛出异常

使用get_or_create需要特别注意的地方:

This method is atomic assuming correct usage, correct database configuration, and correct behavior of the underlying database. However, if uniqueness is not enforced at the database level for the kwargs used in a get_or_create call (see unique or unique_together), this method is prone to a race-condition which can result in multiple rows with the same parameters being inserted simultaneously.

渣翻译:该方法如果在正确的使用,正确的数据库配置以及底层数据库正确的操作下是原子性的,但是,如果unique属性没有被强制添加在kwargs参数中的字段上,在调用该方法时很有可能会插入多条相同的数据。

1
2
3
4
5
6
7
8
9
10
class TestGetOrCreate(View):
def get(self, request):
for i in range(10):
t = threading.Thread(target=get_or_create_product, args=())
t.start()


def get_or_create_product():
p, created = Product.objects.get_or_create(category_id=1, product_sn="product_sn", click_num=50)
print created

Alt text

上面的代码开启10个线程,几乎同时执行get_or_create()方法。可以看到在数据库中创建了2条数据,时间间隔约为0.006秒。所以,get_or_create()方法在并发处理的情形下会出现重复插入的问题。
那么,如果将product_sn字段添加unique约束会什么样的情况呢?

顺带一提,在Django中若某一个table中已有数据,需要将某一个字段修改为unique=True时,必须保证该字段在库中没有重复记录,否则在migrate时会抛出异常。确认无重复后进行makemigrations以及migrate操作是没有问题的。1062, “Duplicate entry ‘’ for key ‘products_product_created_aaa0463f_uniq’”`

product_sn字段添加unique约束后运行相同的代码,程序没有抛出异常,且数据库中只有一条数据。

在官方文档中,还有这么一段话:

If you are using MySQL, be sure to use the READ COMMITTED isolation level rather than REPEATABLE READ (the default), otherwise you may see cases where get_or_create will raise an IntegrityError but the object won’t appear in a subsequent get() call.

渣翻译又来了:如果你在使用MySQL数据库,请确保使用READ COMMITTED隔离级别而不是默认的REPEATABLE READ,否则你可能会看到get_or_create()方法抛出IntegrityError这个异常但是对象不会出现在作为结果发生的get()方法。

所以,在使用get_or_create()方法时,需要限制某一个字段的unique

3. update_or_create(defaults=None, **kwargs)

原理和get_or_create()方法相似,只不过为更新或创建。同样的,这个方法在没有unique的约束下可能会创建出重复数据。

4. bulk_create(objs, batch_size=None)

This method inserts the provided list of objects into the database in an efficient manner (generally only 1 query, no matter how many objects there are)

高效的批量插入数据

1
2
3
4
Entry.objects.bulk_create([
... Entry(headline='This is a test'),
... Entry(headline='This is only a test'),
... ])

然而这个东西目前没使用过……

5. count()

Returns an integer representing the number of objects in the database matching the QuerySet. The count() method never raises exceptions.

永远不会抛异常的方法…..
在实际生产中如果想要判断某条查询返回的数量,尽量使用count()方法, 而不是len()方法。因为len()方法是将所有符合查询条件的数据全部返回,之后在计算数组的长度,而count()方法相当于select count(*),会更加高效。

6. update(**kwargs)

注意,该方法仅作用于QuerySets结果集,即filter(), get()方法不能使用.update()更新数据。

1
p = Product.objects.filter(id=1).update(click_num=232323)

当然,update()方法同样可以传入整个字典来进行更新。

1
2
defaults = {"click_num": 888, "category_id": 2}
p = Product.objects.filter(id=1).update(**defaults)

等下,如果defaults字典里面的某一个字段Product中没有会发生什么?例如username字段

1
2
3
defaults = {"click_num": 888, "category_id": 2, "username": "smart"}
p = Product.objects.filter(id=1).update(**defaults)
# p为更新数据的条数

无情的抛出了异常:FieldDoesNotExist: Product has no field named 'username'
所以在使用字典进行更新时要特别注意模型中和字典中的字段是否匹配

If you’re just updating a record and don’t need to do anything with the model object, the most efficient approach is to call update(), rather than loading the model object into memory. For example, instead of doing this:

1
2
3
e = Entry.objects.get(id=10)
e.comments_on = False
e.save()

…do this:

1
Entry.objects.filter(id=10).update(comments_on=False)

7. delete()

delete()方法简单粗暴,不需要传递任何参数,直接干掉,同时也会删掉与之关联的外键

1
2
3
4
5
6
7
p = Product.objects.get(id=1)
p.delete()
# 单个删除

p = Product.objects.filter(id=1)
p.delete()
# 批量删除


Django中的model关系梳理

其实这里面的关系非常简单, ORM已经为查询做了相当多的抽象工作,只需要按照它所定义的API调用就好了.

model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from django.db import models

from django.contrib.auth.models import User


class AppUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)

nickname = models.CharField(max_length=100)
age = models.IntegerField()
avatar_url = models.URLField(max_length=500)
short_desc = models.CharField(max_length=100)


class Order(models.Model):
order_num = models.CharField(max_length=36, unique=True)
total_price = models.FloatField()

app_user = models.ForeignKey(AppUser) # 注意这里并没有添加related_name

created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)


class OrderProduct(models.Model):
"""
这里多扯一句,之所以需要重新创建一个OrderProduct而不是Product是因为我们需要在订单中存入Product的一个当时的快照信息
即Product里面的商品信息是会被修改的,那么一旦修改Product就会造成存入的数据完全无效
故这里需要数据冗余
"""
title = models.CharField(max_length=500)
price = models.FloatField()
nums = models.IntegerField()
small_image = models.URLField(max_length=500)

order = models.ForeignKey(Order, related_name="order_product")

查询

  • 根据User.username查询该用户的所有订单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
常规:
user = User.object.get(username="smart")
order_list = user.appuser.order_set.all()
说明:
1. 为什么可以使用user.appuser这种看起来不是那么正常的东西
可以使用dir(user)来查看这个对象的所有属性和方法,可以找到`appuser`这么个东西
2. 为什么会有aapuser
AppUser和User属于一对一关系,Django在处理model之间的关系时会默认使用所关联的那个model的小写名称作为关联查询的依据.
3. user.appuser那么就是AppUser的一个实例,那order_set又是个什么?
AppUser和Order为一对多关系,Django在处理这种关系时会将所关联的对象的model名称+"_set"作为查询依据.如果在`app_user = models.ForeignKey(AppUser)`中添加了related_name参数的话,那么该`order_set`就会变成你所定义的related_name.此时appuser中就不再有`order_set`这个东西了.

不是那么常规的写法:
order_list = Order.objects.filter(appuser__user__username="smart")
其中第一个"__"表示外键连接,第二个"__"表示属性查询,相当于`user.username`
order_list = Order.objects.filter(appuser__user__username__contains="smart")\
查询username中包含`smart`的用户的所有订单
  • 查询指定订单的商品
1
2
3
4
5
# 因为在OrderProduct.order中定义了related_name, 那么就可以使用这个名称进行查询
product_list = Order.objects.get(id=1).order_product.all()
or
product_list = OrderProduct.objects.filter(order__id=1)
上面两条语句是等价的.
  • 查询订单商品编号为1的商品是哪个用户买的
1
2
3
user = OrderProduct.objects.get(id=1).order.app_user.user
or
user = User.objects.get(appuser__order__order_product__id=1)

从获取到的结果上来看,通过反向查询也好,还是常规思维的链式查询也罢,获得到的数据都是一样的.但是,这两种查询机制在SQL层面是完全不同的.
django.db.connection这个实例中有一个属性queries能够记录到执行的SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import connection

from django.contrib.auth.models import User
from relationship.models import OrderProduct

user = OrderProduct.objects.get(id=1).order.app_user.user
connection.queries
Out[7]:
[{'sql': 'SELECT @@SQL_AUTO_IS_NULL', 'time': '0.001'},
{'sql': 'SELECT VERSION()', 'time': '0.001'},
{'sql': 'SELECT `relationship_orderproduct`.`id`, `relationship_orderproduct`.`title`, `relationship_orderproduct`.`price`, `relationship_orderproduct`.`nums`, `relationship_orderproduct`.`small_image`, `relationship_orderproduct`.`order_id` FROM `relationship_orderproduct` WHERE `relationship_orderproduct`.`id` = 1',
'time': '0.002'},
{'sql': 'SELECT `relationship_order`.`id`, `relationship_order`.`order_num`, `relationship_order`.`total_price`, `relationship_order`.`app_user_id`, `relationship_order`.`created`, `relationship_order`.`updated` FROM `relationship_order` WHERE `relationship_order`.`id` = 1',
'time': '0.001'},
{'sql': 'SELECT `relationship_appuser`.`id`, `relationship_appuser`.`user_id`, `relationship_appuser`.`nickname`, `relationship_appuser`.`age`, `relationship_appuser`.`avatar_url`, `relationship_appuser`.`short_desc` FROM `relationship_appuser` WHERE `relationship_appuser`.`id` = 1',
'time': '0.001'},
{'sql': 'SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1',
'time': '0.002'}]

可以看到这个查询一共执行了4条SQL语句.
再来看采用外键连接的查询方式:

1
2
3
4
5
6
7
8
user = User.objects.get(appuser__order__order_product__id=1)
connection.queries

Out[5]:
[{'sql': 'SELECT @@SQL_AUTO_IS_NULL', 'time': '0.001'},
{'sql': 'SELECT VERSION()', 'time': '0.001'},
{'sql': 'SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` INNER JOIN `relationship_appuser` ON (`auth_user`.`id` = `relationship_appuser`.`user_id`) INNER JOIN `relationship_order` ON (`relationship_appuser`.`id` = `relationship_order`.`app_user_id`) INNER JOIN `relationship_orderproduct` ON (`relationship_order`.`id` = `relationship_orderproduct`.`order_id`) WHERE `relationship_orderproduct`.`id` = 1',
'time': '0.002'}]

这里便于查看,重新开了一个shell来测试,connection.queries会记录当前shell所执行的所有语句.
那么很明显的可以看到这种查询方式就只有一条内连接的SQL语句,从查询效率上来讲,聚合查询的效率要高于多条SQL语句查询.所以在效率方面选择外键连接查询更加合适一些.

ManyToMany

留坑,后面有时间再写.