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

时间:2011-04-26 19:35:58

标签: python sqlalchemy eager-loading

我有一些数据库结构;因为大部分内容与我们无关,我只会描述一些相关内容。我们以湖项目对象为例:

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查询。为避免这种情况,我为项目对象使用了2个自定义地图:

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将在单个查询中加入组,其项目和相应的元组。如果我使用默认延迟加载来访问它们,sqlalchemy将需要发出1个查询以获取项目+ 1以获取列表中所有项目的组+ 1 *#项目+ 1 *#项目以获取每个项目的元组,这很浪费。

2.1)但是如果我已经获取了Item对象,并且我想加载的一些属性已经加载了怎么办?据我所知,当我从数据库中重新获取某个对象时 - 它已经加载的关系不会被卸载,我是否正确?

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

2.3)如果我从数据库中获取了以下内容:原始项目,item.group和item.group.items列表中的部分项目,其中一些项目可能已加载元组,哪些是最佳策略数据结构与上面的eager list相同:重新获取具有(“items”,“items.metaGroup”)组的组,或者单独检查项目列表中的每个项目,如果项目或其metaGroup未加载 - 加载它们?它似乎取决于具体情况,因为如果一切都已经加载了 - 发出如此繁重的查询是毫无意义的。 sqlalchemy是否提供了一种方法来跟踪是否加载了某个对象关系,并且能够比仅仅一个级别更深入地查看?

作为2.3的说明 - 我可以获取ID为83的组,急切地获取“items”和“items.metaGroup”。有没有办法从一个项目(其groupID为83)确定,是否使用sqlalchemy工具加载了“group”,“group.items”和“group.items.metaGroup”(在这种情况下全部他们应该加载??

2 个答案:

答案 0 :(得分:6)

要强制加载延迟属性,只需访问它们即可。这是最简单的方法,它适用于关系,但对于Column来说效率不高(您将在同一个表中为每个列获得单独的SQL查询)。您可以从sqlalchemy.orm.attributes.instance_state(obj).unloaded获取所有已卸载属性(关系和列)的列表。

您不在示例中使用延迟列,但为了完整起见,我将在此处对其进行描述。处理延迟列的典型方案如下:

  • 使用deferred()装饰选定的列。使用group参数deferred()将它们合并为一个或多个组。
  • 根据需要在查询中使用undefer()undefer_group()选项。
  • 访问放入组中的延迟列将加载此组中的所有列。

不幸的是,这不起作用:您可以将列组合成组而不会默认使用column_property(Column(…), group=…)延迟加载,但defer()选项不会影响它们(它适用于{{1只有,而不是列属性,至少在0.6.7中。

强制加载Nathan Villaescusa建议的延迟列属性Column可能是最好的解决方案。我看到的唯一缺点是它首先使属性失效,因此你必须确保在session.refresh(obj, attribute_names=…)参数之间没有加载属性(例如通过使用与attribute_names的交集)。

<强>更新

1)SQLAlchemy会跟踪加载的对象。这就是ORM的工作原理:会话中必须有每个身份的唯一对象。默认情况下,它的内部缓存很弱(使用state.unloaded来更改它),因此只要代码中没有对象,就会从缓存中清除对象。当对象已经在会话中时,SQLAlchemy不会对weak_identity_map=False执行SQL请求。但这仅适用于query.get(pk)方法,因此get()将在会话中使用加载的数据执行SQL请求和刷新对象。

2)急切加载关系会导致请求减少,但并不总是更快。您必须检查数据库和数据。

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

2.2)query.filter_by(id=pk).first()使用item.group方法加载,因此如果对象已经在会话中,则不会导致SQL请求。

2.3)是的,这取决于具体情况。对于大多数情况,最好的是希望SQLAlchemy将使用正确的策略:)。对于已经加载的关系,您可以检查相关对象的关系是否通过query.get()加载,并递归到任何深度。但是当尚未加载关系时,您无法知道相关对象及其关系是否已经加载:即使尚未加载关系,相关对象[s]可能已经在会话中(只是想象您请求第一项,加载其组,然后请求具有相同组的其他项目)。对于您的特定示例,我认为只需递归检查state.unloaded即可。

答案 1 :(得分:3)

1) 来自Session documentation

  

[会话]在某种程度上用作缓存   它实现了身份地图   模式,并存储键入的对象   他们的主要钥匙。但事实并非如此   做任何类型的查询缓存。 ... 这只是   当你说query.get({some primary   密钥})会话不必   发出查询。

2.1)你是对的,刷新对象时不会修改关系。

2.2)是的,该小组将在身份图中。

2.3)我相信你最好的选择是尝试在一个查询中重新加载整个group.items。根据我的经验,发出一个大请求通常比几个较小的请求快得多。唯一一次只重新加载特定的group.item才有意义,其中只有一个需要加载。虽然在这种情况下你正在做一个大型查询而不是一个小查询,所以你实际上并没有减少查询次数。

我没有尝试过,但我相信您应该能够使用sqlalchemy.orm.util.identity_key方法来确定对象是否在sqlalchemy的标识图中。我很想知道调用identiy_key(Group,83)返回的内容。

初步问题) 如果我理解正确,你有一个从数据库中获取的对象,其中某些关系是eagerloaded,你想用单个查询获取其余的关系?我相信您可以使用传递要加载的关系名称的Session.refresh()方法。