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

Reply via email to