在SQLAlchemy中完成对象及其关系,避免不必要的查询

7 投票
2 回答
5010 浏览
提问于 2025-04-16 16:28

我有一个数据库结构,虽然大部分内容对我们来说不太相关,但我会描述一些重要的部分。我们以Item对象为例:

items_table = Table("invtypes", gdata_meta,
                    Column("typeID", Integer, primary_key = True),
                    Column("typeName", String, index=True),
                    Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
                    Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True))

mapper(Item, items_table,
       properties = {"group" : relation(Group, backref = "items"),
                     "_Item__attributes" : relation(Attribute, collection_class = attribute_mapped_collection('name')),
                     "effects" : relation(Effect, collection_class = attribute_mapped_collection('name')),
                     "metaGroup" : relation(MetaType,
                                            primaryjoin = metatypes_table.c.typeID == items_table.c.typeID,
                                            uselist = False),
                     "ID" : synonym("typeID"),
                     "name" : synonym("typeName")})

我想在sqlalchemy和数据库层面上提高一些性能,有几个想法:

1) 请求同一个项目两次:

item = session.query(Item).get(11184)
item = None (reference to item is lost, object is garbage collected)
item = session.query(Item).get(11184)

每次请求都会生成并发送一个SQL查询。为了避免这种情况,我使用了两个自定义的映射来处理一个项目对象:

itemMapId = {}
itemMapName = {}

@cachedQuery(1, "lookfor")
def getItem(lookfor, eager=None):
    if isinstance(lookfor, (int, float)):
        id = int(lookfor)
        if eager is None and id in itemMapId:
            item = itemMapId[id]
        else:
            item = session.query(Item).options(*processEager(eager)).get(id)
            itemMapId[item.ID] = item
            itemMapName[item.name] = item
    elif isinstance(lookfor, basestring):
        if eager is None and lookfor in itemMapName:
            item = itemMapName[lookfor]
        else:
            # Items have unique names, so we can fetch just first result w/o ensuring its uniqueness
            item = session.query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
            itemMapId[item.ID] = item
            itemMapName[item.name] = item
    return item

我相信sqlalchemy也有类似的对象跟踪功能,至少是通过主键(item.ID)来跟踪。如果是这样的话,我可以清空这两个映射(虽然清空名称映射需要对使用这些查询的应用程序进行一些小修改),这样就不需要重复功能,可以使用默认的方法。我的实际问题是:sqlalchemy中是否有这样的功能,如何访问它?

2) 提前加载关系通常可以节省很多数据库请求。比如说,我肯定需要以下一组item=Item()的属性:

item.group (Group object, according to groupID of our item)
item.group.items (fetch all items from items list of our group)
item.group.items.metaGroup (metaGroup object/relation for every item in the list)

如果我有某个项目的ID,但还没有加载这个项目,我可以从数据库请求它,同时提前加载我需要的所有内容:sqlalchemy会在一个查询中连接组、它的项目和相应的metaGroups。如果我使用默认的懒加载方式访问它们,sqlalchemy就需要发出1个查询来获取一个项目 + 1个查询来获取组 + 1*#items来获取列表中的所有项目 + 1*#items来获取每个项目的metaGroup,这样就浪费了。

2.1) 但是如果我已经获取了Item对象,并且我想加载的一些属性已经被加载了呢?据我理解,当我从数据库重新获取某个对象时,它已经加载的关系不会被卸载,对吗?

2.2) 如果我已经获取了Item对象,并想访问它的组,我可以直接使用item.groupID来调用getGroup,并应用我需要的任何提前加载的语句(比如“items”和“items.metaGroup”)。这样应该可以正确加载组及其请求的关系,而不影响项目的内容。sqlalchemy会正确地将这个获取的组映射到item.group上吗?这样当我访问item.group时,就不会从底层数据库再获取任何东西了?

2.3) 如果我从数据库获取了以下内容:原始项目、项目组以及项目组.items列表中的一部分项目,其中一些可能已经加载了metaGroup,那么完成数据结构以与上面的提前加载列表相同的最佳策略是什么呢?是重新获取组并提前加载(“items”和“items.metaGroup”),还是逐个检查items列表中的每个项目,如果项目或其metaGroup没有加载,就加载它们?这似乎取决于具体情况,因为如果所有内容早已加载,那么发出这样一个重查询就没有意义。sqlalchemy是否提供了一种方法来跟踪某个对象关系是否已加载,并且能够深入查看超过一层的内容?

作为2.3的一个例子,我可以获取ID为83的组,同时提前加载“items”和“items.metaGroup”。有没有办法从一个项目(它的groupID是83)判断它是否已经加载了“group”、“group.items”和“group.items.metaGroup”,使用sqlalchemy的工具(在这种情况下,所有这些都应该已经加载)?

2 个回答

4

1) 从Session文档中可以了解到:

[Session] 有点像一个缓存,因为它实现了身份映射模式,并且将对象存储在以主键为索引的方式中。不过,它并不进行任何查询缓存。... 只有当你使用 query.get({某个主键}) 时,Session 才不需要发出查询。

2.1) 你说得对,刷新一个对象时,关系不会被修改。

2.2) 是的,组会在身份映射中。

2.3) 我认为你最好尝试一次性加载整个组的项目,而不是分开加载。根据我的经验,发出一个大请求通常比发出几个小请求要快得多。只有在确实只需要加载一个特定的组项目时,才有必要单独加载。不过在这种情况下,你实际上是发出了一个大查询,而不是一个小查询,所以并没有减少查询的数量。

我还没有尝试过,但我相信你可以使用sqlalchemy.orm.util.identity_key 方法来判断一个对象是否在sqlalchemy的身份映射中。我很想知道调用 identity_key(Group, 83) 会返回什么。

最初的问题) 如果我理解正确的话,你有一个从数据库中获取的对象,其中一些关系是预加载的,而你想用一个查询获取其余的关系?我认为你可以使用Session.refresh() 方法,并传入你想要加载的关系名称。

7

要强制加载懒加载的属性,只需访问它们。这是最简单的方法,对于关系来说效果很好,但对于Column来说效率就没那么高了(每个列都会单独发出SQL查询)。你可以通过sqlalchemy.orm.attributes.instance_state(obj).unloaded获取所有未加载属性的列表(包括关系和列)。

虽然你在例子中没有使用延迟加载的列,但我还是在这里简单介绍一下。处理延迟加载列的典型场景如下:

  • deferred()装饰选定的列。可以通过group参数将它们组合成一个或多个组。
  • 在查询时使用undefer()undefer_group()选项。
  • 访问放在组里的延迟加载列会加载该组中的所有列。

不幸的是,这个过程不能反向操作:你可以通过column_property(Column(…), group=…)将列组合成组,而不默认延迟加载它们,但defer()选项不会影响这些列(至少在0.6.7版本中,它只对Column有效,而不适用于列属性)。

要强制加载延迟加载的列属性,session.refresh(obj, attribute_names=…)是Nathan Villaescusa提出的最佳解决方案。我看到的唯一缺点是,它会先使属性失效,所以你需要确保传递的attribute_names参数中没有已加载的属性(例如,可以通过与state.unloaded的交集来检查)。

更新

1) SQLAlchemy确实会跟踪已加载的对象。这就是ORM的工作原理:每个身份在会话中只能有一个对象。它的内部缓存默认是弱引用(可以通过weak_identity_map=False来改变),所以一旦你的代码中没有对它的引用,该对象就会从缓存中移除。当对象已经在会话中时,SQLAlchemy不会对query.get(pk)发出SQL请求。但这只适用于get()方法,因此query.filter_by(id=pk).first()会发出SQL请求并用加载的数据刷新会话中的对象。

2) 预加载关系会减少请求次数,但并不总是更快。你需要根据你的数据库和数据来检查这一点。

2.1) 从数据库重新获取数据不会卸载通过关系绑定的对象。

2.2) item.group是通过query.get()方法加载的,所以如果对象已经在会话中,就不会发出SQL请求。

2.3) 是的,这取决于具体情况。在大多数情况下,最好的办法是希望SQLAlchemy会使用正确的策略 :)。对于已经加载的关系,你可以通过state.unloaded检查相关对象的关系是否已加载,甚至可以递归检查到任意深度。但当关系尚未加载时,你无法知道相关对象及其关系是否已经加载:即使关系尚未加载,相关对象可能已经在会话中(想象一下你请求第一个项目,加载它的组,然后请求另一个与同一组相关的项目)。对于你的具体例子,我认为递归检查state.unloaded没有问题。

撰写回答