对象关系映射( ORM )使应用程序开发人员的工作变得更轻松,这在很大程度上要归功于它,它使您可以使用您可能会使用的语言(例如Python)而不是原始SQL查询与数据库进行交互。 SQLAlchemy是一个Python ORM工具包,它提供了使用Python对SQL数据库的访问。 它是一种成熟的ORM工具,它增加了模型关系,强大的查询构造范例,易于序列化等优点。 但是,它的易用性使其很容易忘记幕后发生的事情。 看起来,使用SQLAlchemy做出的小选择可能会对性能产生重要影响。
本文介绍了开发人员在使用SQLAlchemy时遇到的一些顶级性能问题,以及如何解决这些问题。
只需要计数时检索整个结果集
有时,开发人员只需要对结果进行计数,但不是利用数据库计数,而是获取所有结果,并使用python中的len完成计数。
count = len ( User. query . filter_by ( acct_active = True ) . all ( ) )
相反,使用SQLAlchemy的count方法将在服务器端进行计数,从而导致发送给客户端的数据少得多。 在前面的示例中调用all()还会导致模型对象的实例化,如果有足够的行,这可能会很快变得昂贵。
除非需要的计数多,否则只需使用count方法即可。
count = User. query . filter_by ( acct_active = True ) . count ( )
仅需要几列即可检索整个模型
在许多情况下,发出查询时只需要几列。 而不是返回整个模型实例,SQLAlchemy只能获取您感兴趣的列。这不仅减少了发送的数据量,而且避免了实例化整个对象的需要。 使用列数据的元组而不是模型可以更快。
result
= User.
query .
all
(
)
for
user
in result:
print
(
user .
name
,
user .
email
)
而是使用with_entities方法仅选择所需的内容 。
result
= User.
query .
with_entities
( User.
name
, User.
email
) .
all
(
)
for
( username
,
email
)
in result:
print
( username
,
email
)
循环内一次更新一个对象
避免使用循环来单独更新集合。 尽管数据库可以非常快速地执行单个更新,但应用程序与数据库服务器之间的往返时间将Swift增加。 通常,在合理的情况下,争取减少查询量。
for
user
in users_to_update:
user .
acct_active
=
True
db.
session .
add
(
user
)
请使用批量更新方法。
query
= User.
query .
filter
(
user .
id .
in_
(
[
user .
id
for
user
in users_to_update
]
)
)
query.
update
(
{
"acct_active" :
True
}
, synchronize_session
=
False
)
触发级联删除
ORM允许在模型上轻松配置关系,但是有些微妙的行为可能令人惊讶。 大多数数据库通过外键和各种级联选项维护关系完整性。 SQLAlchemy允许您使用外键和级联选项定义模型,但是ORM具有自己的级联逻辑,可以抢占数据库。
考虑以下模型。
class Artist
( Base
) :
__tablename__
=
"artist"
id
= Column
( Integer
, primary_key
=
True
)
songs
= relationship
(
"Song"
, cascade
=
"all, delete"
)
class Song
( Base
) :
__tablename__
=
"song"
id
= Column
( Integer
, primary_key
=
True
)
artist_id
= Column
( Integer
, ForeignKey
(
"artist.id"
, ondelete
=
"CASCADE"
)
)
删除对Song表的查询,从而防止由于外键导致删除操作。
这种行为可能会成为复杂关系和大量记录的瓶颈。
包括passive_deletes选项以确保数据库正在管理关系。 但是,请确保您的数据库具有此功能。 例如,SQLite默认情况下不管理外键。
songs = relationship ( "Song" , cascade = "all, delete" , passive_deletes = True )
当应该使用急切加载时依赖于延迟加载
延迟加载是关系的默认SQLAlchemy方法。 从最后一个示例开始构建,这意味着加载艺术家不会同时加载他或她的歌曲。 通常这是一个好主意,但是如果始终需要加载某些关系,则单独的查询可能会很浪费。
如果允许以惰性方式加载关系,则诸如棉花糖之类的流行序列化框架可以触发一系列查询。
有几种方法可以控制此行为。 最简单的方法是通过关系函数本身。
songs = relationship ( "Song" , lazy = "joined" , cascade = "all, delete" )
这将导致左联接被添加到对艺术家的任何查询中,因此, 歌曲集合将立即可用。 尽管有更多数据返回给客户端,但往返次数可能会少得多。
SQLAlchemy为无法采用这种全面方法的情况提供了更细粒度的控制。 joinload()函数可用于在每个查询的基础上切换联合加载。
from sqlalchemy.
orm
import joinedload
artists
= Artist.
query .
options
( joinedload
( Artist.
songs
)
)
print
( artists.
songs
)
# Does not incur a roundtrip to load
使用ORM进行批量记录导入
导入数千条记录时,构造完整模型实例的开销成为主要瓶颈。 例如,想象一下,从一个文件中载入了数千首歌曲记录,其中每首歌曲都首先被转换成字典。
for song
in songs:
db.
session .
add
( Song
( **song
)
)
而是绕过ORM,仅使用核心SQLAlchemy的参数绑定功能。
batch
=
[
]
insert_stmt
= Song.__table__.
insert
(
)
for song
in songs:
if
len
( batch
)
>
1000 :
db.
session .
execute
( insert_stmt
, batch
)
batch.
clear
(
)
batch.
append
( song
)
if batch:
db.
session .
execute
( insert_stmt
, batch
)
请记住,此方法自然会跳过您可能依赖的任何客户端ORM逻辑,例如基于Python的列默认值。 尽管此方法比作为完整模型实例加载对象要快,但是您的数据库可能具有更快的批量加载方法。 例如,PostgreSQL具有COPY命令,该命令可能为加载大量记录提供最佳性能。
提前调用提交或刷新
在许多情况下,您需要将子记录与其父记录相关联,反之亦然。 一种明显的方法是刷新会话,以便为有问题的记录分配一个ID。
artist
= Artist
( name
=
"Bob Dylan"
)
song
= Song
( title
=
"Mr. Tambourine Man"
)
db.
session .
add
( artist
)
db.
session .
flush
(
)
song.
artist_id
= artist.
id
每个请求提交或刷新多次通常是不必要的,也是不希望的。 数据库刷新涉及强制在数据库服务器上进行磁盘写入,并且在大多数情况下,客户端将阻塞,直到服务器可以确认已写入数据为止。
SQLAlchemy可以跟踪关系并在后台管理键。
artist
= Artist
( name
=
"Bob Dylan"
)
song
= Song
( title
=
"Mr. Tambourine Man"
)
artist.
songs .
append
( song
)
结语
我希望此常见陷阱列表可以帮助您避免这些问题并使应用程序平稳运行。 与往常一样,在诊断性能问题时,测量是关键。 大多数数据库都提供性能诊断,可以帮助您查明问题,例如PostgreSQL pg_stat_statements模块。
翻译自: https://opensource.com/article/19/9/common-pitfalls-python