Hi *,
I am trying to cache SQLAlchemy queries in memory for a rich client
application. To invalidate the cache for changes seen in the database, I
am trying to drop in-memory instances that have been changed or deleted.
This requires comparing the identity of the deleted objects with
in-memory objects. I tried using identity_key for this and failed,
because it tries to reload from the database and I expire the instances
when I am told they had some changes.
The attached IPython notebook shows the behaviour. Short summary:
Reloads expired state (potential ObjectDeletedError)
identity_key(instance=instance)
mapper.identity_key_from_instance(instance)
mapper.primary_key_from_instance(instance)
Uses old primary key (no reload, no ObjectDeletedError)
object_state(user).identity_key
object_state(user).identity
object_state(user).key
The main reason why I care is that identity_key may generate database
queries which kill any performance improvement of my query cache.
I think this should be documented in SQLAlchemy, I did not expect those
functions to ever raise an exception.
Please consider extending the documentation via my attached patch (also
adds a unit test for the ObjectDeletedError).
Greetings, Torsten
--
DYNAmore Gesellschaft fuer Ingenieurdienstleistungen mbH
Torsten Landschoff
Office Dresden
Tel: +49-(0)351-312002-10
Fax: +49-(0)351-312002-29
mailto:torsten.landsch...@dynamore.de
http://www.dynamore.de
DYNAmore Gesellschaft für FEM Ingenieurdienstleistungen mbH
Registration court: Stuttgart, HRB 733694
Managing director: Prof. Dr. Karl Schweizerhof, Dipl.-Math. Ulrich Franz
--
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/groups/opt_out.
{
"metadata": {
"name": ""
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "code",
"collapsed": false,
"input": [
"import sqlalchemy\n",
"sqlalchemy.__version__"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 1,
"text": [
"'0.9.0'"
]
}
],
"prompt_number": 1
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from sqlalchemy import Column, Integer, String, create_engine\n",
"from sqlalchemy.orm import sessionmaker\n",
"from sqlalchemy.orm.util import identity_key, object_state,
object_mapper, class_mapper\n",
"from sqlalchemy.ext.declarative import declarative_base\n",
"\n",
"Base = declarative_base()\n",
"\n",
"class User(Base):\n",
" __tablename__ = 'users'\n",
"\n",
" id = Column(Integer, primary_key=True)\n",
" name = Column(String(50))\n",
"\n",
"engine = create_engine(\"sqlite:///\")\n",
"Base.metadata.create_all(engine)\n",
"\n",
"Session = sessionmaker(engine)\n",
"session = Session()\n",
"\n",
"user = User(name=\"Joe\")\n",
"session.add(user)\n",
"session.commit()"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"outputs": [],
"prompt_number": 2
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"identity_key(instance=user)"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 3,
"text": [
"(__main__.User, (1,))"
]
}
],
"prompt_number": 3
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"object_state(user).identity_key"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 4,
"text": [
"(__main__.User, (1,))"
]
}
],
"prompt_number": 4
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"object_state(user).identity"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 5,
"text": [
"(1,)"
]
}
],
"prompt_number": 5
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"object_state(user).key"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 6,
"text": [
"(__main__.User, (1,))"
]
}
],
"prompt_number": 6
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"mapper = class_mapper(User)"
],
"language": "python",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"prompt_number": 7
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"mapper.identity_key_from_instance(user)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 8,
"text": [
"(__main__.User, (1,))"
]
}
],
"prompt_number": 8
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"mapper.primary_key_from_instance(user)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 9,
"text": [
"[1]"
]
}
],
"prompt_number": 9
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"session.execute(User.__table__.delete())\n",
"session.commit()"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 10
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"try:\n",
" identity_key(instance=user)\n",
"except Exception, e:\n",
" print \"%s: %s\" % (type(e).__name__, e)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"ObjectDeletedError: Instance '<User at 0x2935a10>' has been deleted,
or its row is otherwise not present.\n"
]
}
],
"prompt_number": 11
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"object_state(user).identity_key"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 12,
"text": [
"(__main__.User, (1,))"
]
}
],
"prompt_number": 12
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"object_state(user).identity"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 13,
"text": [
"(1,)"
]
}
],
"prompt_number": 13
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"object_state(user).key"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 14,
"text": [
"(__main__.User, (1,))"
]
}
],
"prompt_number": 14
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"try:\n",
" mapper.identity_key_from_instance(user)\n",
"except Exception, e:\n",
" print \"%s: %s\" % (type(e).__name__, e)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"ObjectDeletedError: Instance '<User at 0x2935a10>' has been deleted,
or its row is otherwise not present.\n"
]
}
],
"prompt_number": 15
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"try:\n",
" mapper.primary_key_from_instance(user)\n",
"except Exception, e:\n",
" print \"%s: %s\" % (type(e).__name__, e)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"ObjectDeletedError: Instance '<User at 0x2935a10>' has been deleted,
or its row is otherwise not present.\n"
]
}
],
"prompt_number": 16
},
{
"cell_type": "code",
"collapsed": false,
"input": [],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 16
}
],
"metadata": {}
}
]
}
>From 7148c9f992719cfbd68731d8a1b6262d9bf195ee Mon Sep 17 00:00:00 2001
From: Torsten Landschoff <torsten.landsch...@dynamore.de>
Date: Fri, 30 Aug 2013 15:23:10 +0200
Subject: [PATCH] Documentation: identity_key and friends query the database
if the target instance has been expired and may even raise
ObjectDeletedError.
New unit test that checks this at least for util.identity_key.
---
lib/sqlalchemy/orm/mapper.py | 8 ++++++--
lib/sqlalchemy/orm/util.py | 4 ++++
test/orm/test_utils.py | 17 ++++++++++++++++-
3 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 30b5ffc..49b69c7 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2225,7 +2225,9 @@ class Mapper(_InspectionAttr):
def identity_key_from_instance(self, instance):
"""Return the identity key for the given instance, based on
- its primary key attributes.
+ its primary key attributes. If the instance's state is expired this
+ will check if the object has been deleted. If that is the case,
+ :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised.
This value is typically also found on the instance state under the
attribute name `key`.
@@ -2245,7 +2247,9 @@ class Mapper(_InspectionAttr):
def primary_key_from_instance(self, instance):
"""Return the list of primary key values for the given
- instance.
+ instance. If the instance's state is expired this
+ will check if the object has been deleted. If that is the case,
+ :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised.
"""
state = attributes.instance_state(instance)
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 9737072..f3f2c05 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -176,6 +176,9 @@ def identity_key(*args, **kwargs):
instance
object instance (must be given as a keyword arg)
+ If the instance's state is expired this will check if the object has
+ been deleted. If that is the case,
+ :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised.
* ``identity_key(class, row=row)``
@@ -196,6 +199,7 @@ def identity_key(*args, **kwargs):
elif len(args) == 2:
class_, ident = args
elif len(args) == 3:
+ # XXX: How can this work? I'd expect a ValueError here -- Torsten Landschoff
class_, ident = args
else:
raise sa_exc.ArgumentError("expected up to three "
diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py
index 9687842..b0ef1cc 100644
--- a/test/orm/test_utils.py
+++ b/test/orm/test_utils.py
@@ -1,5 +1,5 @@
from sqlalchemy.testing import assert_raises, assert_raises_message
-from sqlalchemy.orm import util as orm_util
+from sqlalchemy.orm import util as orm_util, exc as orm_exc
from sqlalchemy import Column
from sqlalchemy import util
from sqlalchemy import Integer
@@ -227,6 +227,21 @@ class IdentityKeyTest(_fixtures.FixtureTest):
key = orm_util.identity_key(User, row=row)
eq_(key, (User, (1,)))
+ def test_identity_key_4(self):
+ users, User = self.tables.users, self.classes.User
+
+ mapper(User, users)
+ s = create_session()
+ u = User(name='u1')
+ s.add(u)
+ s.flush()
+ s.execute(users.delete())
+ s.expire(u)
+ assert_raises(
+ orm_exc.ObjectDeletedError,
+ orm_util.identity_key, instance=u
+ )
+
class PathRegistryTest(_fixtures.FixtureTest):
run_setup_mappers = 'once'
--
1.7.12