前言
ORM,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。下面是一个示例。通过使用 ORM,我们只需要操作 Author 和 Blog 对象,而不用操作相关的数据库表。这里主要介绍一下 Django ORM 的相关使用。
优缺点
使用 ORM 最大的优点就是快速开发,让我们将更多的精力放在业务上而不是数据库上,下面是 ORM 的几个优点
- 隐藏了数据访问细节,使通用数据库交互变得简单易行。同时 ORM 避免了不规范、冗余、风格不统一的 SQL 语句,可以避免很多人为的 bug,方便编码风格的统一和后期维护。
- 将数据库表和对象模型关联,我们只需针对相关的对象模型进行编码,无须考虑对象模型和数据库表之间的转化,大大提高了程序的开发效率。
- 方便数据库的迁移。当需要迁移到新的数据库时,不需要修改对象模型,只需要修改数据库的配置。
ORM 的最令人诟病的地方就是性能问题,不过现在已经提高了很多,下面是 ORM 的几个缺点
- 性能问题
- 自动化进行数据库关系的映射需要消耗系统资源
- 程序员编码
- 在处理多表联查、where 条件复杂的查询时,ORM 可能会生成的效率低下的 SQL
- 通过 Lazy load 和 Cache 很大程度上改善了性能问题
- SQL 调优,SQL 语句是由 ORM 框架自动生成,虽然减少了 SQL 语句错误的发生,但是也给 SQL 调优带来了困难。
- 越是功能强大的 ORM 越消耗内存,因为一个 ORM Object 会带有很多成员变量和成员函数。
- 对象和关系之间并不是完美映射
一般来说 ORM 足以满足我们的需求,如果对性能要求特别高或者查询十分复杂,可以考虑使用原生 SQL 和 ORM 共用的方式
Django ORM
在 Django 框架中集成了 ORM 模块,我们来看下具体的使用,部分内容会给出基于 MySQL 的 SQL 语句。
Manager
在创建完 Model 对象之后,Django 会自动为其关联一个 Manager 对象,该对象是 Model 进行数据库操作的接口。默认的 Manager 对象名称为 objects,下面是使用 Manager 进行增删改查的一个示例:
代码语言:javascript复制def save_blog():
# 使用 get 检索数据时,如果数据不存在,会报 DoesNotExist 错误
# 可以使用 Blog.objects.all().filter(id=1).first() 方法
author = Author.objects.get(id=1)
blog = Blog(title='blog2', content='blog2', author=author)
blog.save()
def update_blog():
blog = Blog.objects.all().get(id=2)
blog.title = 'change_title'
blog.save()
def delete_blog():
blog = Blog.objects.all().filter(id=2).first()
if blog is not None:
blog.delete()
def fetch_blog():
blogs = Blog.objects.all()
for blog in blogs:
print blog
我们可以自定义 Manager 的名称,如下所示:
代码语言:javascript复制from django.db import models
class Person(models.Model):
#...
people = models.Manager()
Person.people.all()
同时我们也可以定义自己的 Manager,为 Manager 加上一些额外的功能,下面的示例会为 Author 添加上所写 Blog 数量信息:
代码语言:javascript复制class AuthorManager(models.Manager):
def with_blog_counts(self, **kwargs):
from django.db import connection
cursor = connection.cursor()
condition = ''
if kwargs.has_key('id'):
condition = 'a.id = %s and ' % kwargs['id']
query = '''
SELECT a.id, a.name, COUNT(b.id)
FROM orm_author a LEFT JOIN orm_blog b
ON a.id = b.author_id
WHERE %s TRUE
GROUP BY a.id
''' % condition
cursor.execute(query)
result_list = []
for row in cursor.fetchall():
author = self.model(id=row[0], name=row[1])
author.blog_count = row[2]
result_list.append(author)
return result_list
class Author(models.Model):
name = models.CharField(max_length=50)
objects = AuthorManager()
authors = Author.objects.with_blog_counts(id=2)
for author in authors:
print author.name, author.blog_count
另外我们也可以为 Model 指定多个 Manager
代码语言:javascript复制class Author(models.Model):
manager = models.Manager()
manager2 = AuthorManager()
QuerySet
从数据库中查询出来的结果一般是一个集合,这个集合称为 QuerySet。QuerySet 有两种来源:通过 Manager 的方法获取、通过 QuerySet 自身的方法获得。Manager 的查询方法和 QuerySet 的方法大部分同名、同意(Manager的就是基于 QuerySet 的实现的),例如 filter, exclude等,但两者也有不同的方法,例如 Manager 的 create、get_or_create,QuerySet 的 delete 等。
基本查询
下面是 QuerySet (也是 Manager的)的几个基本的查询方法
all() - 获得数据库中所有实例的一个 QuerySet
filter(**kwargs) - 返回满足查询条件的 QuerySet
exclude(**kwargs) - 获得不满足查询条件的 QuerySet
get(**kwargs) — 从数据库中获得一个匹配的结果(一个实例),如果没有匹配结果或者匹配结果大于一个都会报错
字段查询
在前面的 filter、exclude 和 get 方法中,我们需要传入参数作为选择条件: title='blog2'
,这个就是字段查询。字段查询的格式如下
field__lookuptype=value # 中间是两个下划线
lookuptype 的类型有下面几种
- exact 精确匹配,默认的 lookup type。上面的
title='blog2'
就相当于title__exact='blog2'
- gt : 大于
- gte : 大于等于
- lt : 小于
- lte :小于等于
- in : in
- contains : 包含,区分大小写 -
a LIKE BINARY '%b%'
- icontains : 包含,不区分大小写 -
a LIKE '%b%'
- iexact : 大小写不敏感的精确匹配 -
a LIKE 'b'
- startswith : 匹配开头,区分大小写 -
a LIKE BINARY 'b%'
- istartswith : 匹配开头,不区分大小写 -
a LIKE 'b%'
- endswith : 匹配结尾,区分大小写 -
a LIKE BINARY '%b'
- iendswith : 匹配结尾,不区分大小写 -
a LIKE '%b'
我们还可以进行关联查询,下面的例子是查询所有 author name 为 zjk 的 blog,
代码语言:javascript复制blogs = Blog.objects.filter(author__name='zjk')
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` INNER JOIN `orm_author` ON (`orm_blog`.`author_id` = `orm_author`.`id`)
# WHERE `orm_author`.`name` = 'zjk'
限制 QuerySet
有时候我们并不需要获取查询集的全部数据,而只需要一个子集,一个常见的场景就是进行分页查询。使用 Python 的切片语法可以限制 QuerySet 的实例数量,ORM 会将翻译成 SQL 的 LIMIT 和 OFFSET 子句,下面是几个例子:
放回 QuerySet 的前 5 个元素
返回 QuerySet 的第 6-10 个元素
使用切片的 step 参数,下面代码返回第 1、3、5、7、9 个元素
如果只要访问一个元素,可以直接用索引来访问:
Lazy load
print q
q = Blog.objects.filter(title='blog2')
q = q.filter(content='blog2')
q = q.exclude(id=3)
# 执行下面的语句才会真正访问数据库
print q
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content` FROM `orm_blog`
# WHERE (`orm_blog`.`title` = 'blog2' AND `orm_blog`.`content` = 'blog2' AND NOT (`orm_blog`.`id` = 3)) LIMIT 21
代码语言:javascript复制blog = Blog.objects.filter(id=3).first()
print blog.title
# 只有执行下面的语句才会访问数据库获取 author 的值,也就是执行第二条 SQL
print blog.author.name
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`id` = 3 ORDER BY `orm_blog`.`id` ASC LIMIT 1
#
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author` WHERE `orm_author`.`id` = 1
迭代:在首次迭代查询集时会执行数据库查询
切片(限制查询集):对查询集执行切片操作时,指定 step 参数
序列化/缓存
repr:对查询集调用 repr 函数
len:对查询集调用 len 函数
list: 对查询集调用 list() 方法强制求值
bool:测试一个查询集的布尔值,例如使用bool(), or, and 或者 if 语句都将导致查询集的求值
缓存
每个 QuerySet 都包含一个缓存来最小化对数据库的访问,下面是一个示例:
代码语言:javascript复制# 下面代码会访问两次数据库
print [blog.title for blog in Blog.objects.all()]
print [blog.content for blog in Blog.objects.all()]
# 下面代码只会访问一次数据库
blogs = Blog.objects.all()
print [blog.title for blog in blogs]
print [blog.content for blog in blogs]
在一个新的 QuerySet 中,缓存为空。当首次对 QuerySet 的所有实例进行求值时,会将查询结果保存到 QuerySet 的缓冲中。当再访问该 QuerySet 时,会直接从缓冲中取数据。
如果只对 QuerySet 的部分实例(query_set[5], query_set[0:10])进行求值,首先会到 QuerySet 的缓冲中查找是否已经缓存了这些实例,如果有就使用缓存值,如果没有就查询数据库,但是不会将查询结果保存到缓冲中。
如果 QuerySet 数量很大不希望被缓存,遍历时使用 iterator 方法:
代码语言:javascript复制blogs = Blog.objects.all()
for blog in blogs.iterator():
print blog.title
关联查询
在讲关联查询之前,首先看一下下面的一个示例。我们前面提到,关联实例是惰性加载的,因此对于下面的代码,每次 for 循环都要访问一次数据库,会严重影响性能。因此我们需要一次将 blog 以及 author 的信息全部取出来,这就是我们马上要讲的关联查询。
代码语言:javascript复制for blog in Blog.objects.all():
print blog.title, blog.author.name
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content` FROM `orm_blog`
#
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author` WHERE `orm_author`.`id` = 1
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author` WHERE `orm_author`.`id` = 1.
# . . . . . .
关联查询就是在查询当前实例的同时,把其关联的实例数据也一块取出来。在下图中 orm_blog 通过一个外键和 orm_author 关联。关联大体上可以分为两种:
- 只有一个关联实例: 外键关联中包含外键的表、OneToOneField,例如下图中的 orm_blog 只与一个 orm_author 的实例关联
- 有多个关联实例:外键关联中不含外键的表、ManyToManyField,例如下图中的 orm_author 就与多个 orm_blog 实例关联
因此 Django ORM 中的关联查询也分两中 select_related(单关联实例) 和 prefetch_related(多关联实例)
select_related
select_related 用来处理单关联实例的情况,适用于 ForeignKey 和 OneToOneField。在查询时,会对关联的表进行 join 操作,取出全部的信息,下面是一个示例:
代码语言:javascript复制blog = Blog.objects.select_related().filter(id=3).first()
print blog.id, blog.author.name
# SQL
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`, `orm_author`.`id`, `orm_author`.`name`
# FROM `orm_blog` INNER JOIN `orm_author` ON (`orm_blog`.`author_id` = `orm_author`.`id`)
# WHERE `orm_blog`.`id` = 3 ORDER BY `orm_blog`.`id` ASC LIMIT 1
select_related 会沿着外键递归查询,例如上图中取表 1 的实例时,会沿着外键将表 3 的数据一块取出来。我们可以传入 depth 参数来指定递归的深度。
如果需要清除 QuerySet 上以前的 select_related 添加的关联字段,可以传入 None 做参数
prefetch_related
prefetch_related 主要适用于 OneTwoMany 和 ManyToManyField。和 select_related 类似,prefetch_related 在查询时会同时取出关联实例的值。与 select_related 不同的是 prefetch_related 不使用 JOIN 方式来查询数据库,而是分别查每个表,最后使用 Python 来实现 JOIN 操作。下面是一个示例:
代码语言:javascript复制author = Author.objects.prefetch_related('blog_set').filter(name='zjk').first()
for blog in author.blog_set.all():
print blog
# SQL:
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author`
# WHERE `orm_author`.`name` = 'zjk' ORDER BY `orm_author`.`id` ASC LIMIT 1
#
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`author_id` IN (1)
如果查询出关联对象的 QuerySet 之后,再对该 QuerySet 执行查询条件,会使该 QuerySet 失效(也就是需要再次访问数据库)。如果在查询关联对象时需要使用查询条件,可以使用 Prefetch 对象,下面是一个示例:
代码语言:javascript复制from django.db.models import Prefetch
authors = Author.objects.prefetch_related(Prefetch(
'blog_set', queryset=Blog.objects.filter(title='blog2'), to_attr='blogs'
))
for author in authors:
print author.blogs
# SQL:
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author`
#
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE (`orm_blog`.`title` = 'blog2' AND `orm_blog`.`author_id` IN (1, 2))
Q 查询
在前面所讲的 filter 和 exclude 方法,对于传入的查询条件都是执行的 AND 操作,如果我们需要对查询条件执行 OR 操作,例如查询 blog 表中 title=‘blog1’ 或者 title=‘blog2’ 的实例,就需要用到 Q 查询。Q 查询支持使用 |、&、~ 操作符,分别对象查询条件的 OR、AND 和 NOT 操作。下面是一个示例:
代码语言:javascript复制from django.db.models import Q
blogs = Blog.objects.filter(Q(id=10) | Q(title='blog2'))
for blog in blogs:
print blog
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE (`orm_blog`.`id` = 10 OR `orm_blog`.`title` = ‘blog2')
F 查询
F 查询主要用来处理表中字段之间的比较,例如查询 blog 表中 title=conent 的记录。同时 F 查询还支持计算(加减乘除)。下面是一个示例:
代码语言:javascript复制from django.db.models import F
blogs = Blog.objects.filter(title=F('content'))
for blog in blogs:
print blog
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`title` = (`orm_blog`.`content`)
blogs = Blog.objects.filter(title=F('content') 2)
for blog in blogs:
print blog
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`title` = ((`orm_blog`.`content` 2))
values 和 values_list
有些时候我们不需要获取实例中所有的数据,而只需要获得几个字段的数据即可,使用 values 和 values_list 可以指定检索的字段。values 会返回一个 dict 数组,而 values_list 会返回 list 数组。下面是一个例子:
代码语言:javascript复制blogs = Blog.objects.filter(id=5).values('title')
print blogs
# <QuerySet [{u'title': u’blog2'}]>
blogs = Blog.objects.filter(id=5).values_list('title')
print blogs
# <QuerySet [(u’blog2',)]>
blogs = Blog.objects.filter(id=5).values_list('title', flat=True)
print blogs
# <QuerySet [u’blog2']>
aggregate 和 annotate
通过 aggregate 和 annotate 可以使用 SQL 的聚合函数,例如 SUM、COUNT、MIN 等。aggregate: 针对所有记录调用聚合函数,返回一个 dict 对象,下面是使用示例:
代码语言:javascript复制from django.db.models import Min
from django.db.models import Sum
result = Blog.objects.aggregate(Min('id'))
print result
# {u'id__min': 3L}
# SELECT MIN(`orm_blog`.`id`) AS `id__min` FROM `orm_blog`
# 自定义属性名
result = Blog.objects.aggregate(total=Sum('id'))
print result
# {'total': Decimal(‘657')}
# SELECT SUM(`orm_blog`.`id`) AS `total` FROM `orm_blog
annotate 先使用 groupby 分组,然后对于每组再调用聚合函数,返回 QuerySet 对象。 annotate 默认按照 id 进行分组,如果需要按其他字段分组,要结合 values /values_list 方法。下面是使用示例:
代码语言:javascript复制#认按照 id 进行分组
blogs = Blog.objects.annotate(Count('title'))
for blog in blogs:
print blog.title__count
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`,
# COUNT(`orm_blog`.`title`) AS `title__count` FROM `orm_blog` GROUP BY `orm_blog`.`id` ORDER BY NULL
# 使用 values 方法,会按照 values 中传入的属性分组
blogs = Blog.objects.values('title').annotate(Count('title'))
for blog in blogs:
print blog['title__count']
# SELECT `orm_blog`.`title`, COUNT(`orm_blog`.`title`) AS `title__count` FROM `orm_blog`
# GROUP BY `orm_blog`.`title` ORDER BY NULL
blogs = Blog.objects.values('title', 'content').annotate(Count('title'))
for blog in blogs:
print blog['title__count']
# SELECT `orm_blog`.`title`, `orm_blog`.`content`, COUNT(`orm_blog`.`title`) AS `title__count`
# FROM `orm_blog` GROUP BY `orm_blog`.`title`, `orm_blog`.`content` ORDER BY NULL
extra
如何一些查询比较复杂可以考虑使用 extra 方法。extra 能在 ORM 生成的 SQL 子句中注入 SQL 代码,语法格式如下:
代码语言:javascript复制# 至少保证一个参数不为空
extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)
select:在 select 子句中插入 SQL 代码
select_params: 设置 select 参数
where: 在 where 子句中插入 SQL 代码
params: 为 where 设置参数
tables: 在 FROM 子句中插入 table 名称
order_by:在 order_by 子句中插入排序字段
原始 SQL 查询
使用 Manager 的 raw 方法可以用于原始的 SQL 查询,并返回 Model 的实例:
代码语言:javascript复制blogs = Blog.objects.raw('select * from orm_blog')
for blog in blogs:
print blog.id , blog.title
如果 SQL 中没有获取某个字段,那么会惰性加载该字段
代码语言:javascript复制# 没有取 title,在后面使用时会访问数据库
blogs = Blog.objects.raw('select id from orm_blog')
for blog in blogs:
print blog.id
print blog.title
# select id from orm_blog
# SELECT `orm_blog`.`id`, `orm_blog`.`title` FROM `orm_blog` WHERE `orm_blog`.`id` = 3
# SELECT `orm_blog`.`id`, `orm_blog`.`title` FROM `orm_blog` WHERE `orm_blog`.`id` = 4
#. . . .
一些优化
如果只需要判断实例是否存在,使用 exists 更高效
如果只需要得到实例的数量,使用 count 函数