diff --git a/include/class.orm.php b/include/class.orm.php
index ef8d7915577a9342b38faf9a5cc0d39b12963ce4..da9ec4b821ee80eea24c0f937230b5830d7d6d52 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -29,10 +29,14 @@ class OrmConfigurationException extends Exception {}
 class ModelMeta implements ArrayAccess {
 
     var $base = array();
+    var $model;
 
     function __construct($model) {
+        $this->model = $model;
         $meta = $model::$meta;
 
+        // TODO: Merge ModelMeta from parent model (if inherited)
+
         if (!$meta['table'])
             throw new OrmConfigurationException(
                 __('Model does not define meta.table'), $model);
@@ -98,6 +102,15 @@ class ModelMeta implements ArrayAccess {
         case 'fields':
             $this->base['fields'] = self::inspectFields();
             break;
+        case 'newInstance':
+            $class_repr = sprintf(
+                'O:%d:"%s":0:{}',
+                strlen($this->model), $this->model
+            );
+            $this->base['newInstance'] = function() use ($class_repr) {
+                return unserialize($class_repr);
+            };
+            break;
         default:
             throw new Exception($what . ': No such meta-data');
         }
@@ -118,6 +131,7 @@ class VerySimpleModel {
     var $ht;
     var $dirty = array();
     var $__new__ = false;
+    var $__deleted__ = false;
     var $__deferred__ = array();
 
     function __construct($row) {
@@ -240,14 +254,50 @@ class VerySimpleModel {
         }
     }
 
+    /**
+     * objects
+     *
+     * Retrieve a QuerySet for this model class which can be used to fetch
+     * models from the connected database. Subclasses can override this
+     * method to apply forced constraints on the QuerySet.
+     */
     static function objects() {
         return new QuerySet(get_called_class());
     }
 
+    /**
+     * lookup
+     *
+     * Retrieve a record by its primary key. This method may be short
+     * circuited by model caching if the record has already been loaded by
+     * the database. In such a case, the database will not be consulted for
+     * the model's data.
+     *
+     * This method can be called with an array of keyword arguments matching
+     * the PK of the object or the values of the primary key. Both of these
+     * usages are correct:
+     *
+     * >>> User::lookup(1)
+     * >>> User::lookup(array('id'=>1))
+     *
+     * For composite primary keys and the first usage, pass the values in
+     * the order they are given in the Model's 'pk' declaration in its meta
+     * data.
+     *
+     * Parameters:
+     * $criteria - (mixed) primary key for the sought model either as
+     *      arguments or key/value array as the function's first argument
+     */
     static function lookup($criteria) {
-        if (!is_array($criteria))
-            // Model::lookup(1), where >1< is the pk value
-            $criteria = array(static::$meta['pk'][0] => $criteria);
+        // Model::lookup(1), where >1< is the pk value
+        if (!is_array($criteria)) {
+            $criteria = array();
+            foreach (func_get_args() as $i=>$f)
+                $criteria[static::$meta['pk'][$i]] = $f;
+        }
+        if ($cached = ModelInstanceManager::checkCache(get_called_class(),
+                $criteria))
+            return $cached;
         return static::objects()->filter($criteria)->one();
     }
 
@@ -258,6 +308,7 @@ class VerySimpleModel {
             if ($ex->affected_rows() != 1)
                 return false;
 
+            $this->__deleted__ = true;
             Signal::send('model.deleted', $this);
         }
         catch (OrmException $e) {
@@ -296,9 +347,10 @@ class VerySimpleModel {
         # Refetch row from database
         # XXX: Too much voodoo
         if ($refetch) {
-            # XXX: Support composite PK
-            $criteria = array($pk[0] => $this->get($pk[0]));
-            $self = static::lookup($criteria);
+            // Uncache so that the lookup will not be short-cirtuited to
+            // return this object
+            ModelInstanceManager::uncache($this);
+            $self = static::lookup($this->get('pk'));
             $this->ht = $self->ht;
         }
         $this->dirty = array();
@@ -358,7 +410,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
     const LOCK_SHARED = 2;
 
     var $compiler = 'MySqlCompiler';
-    var $iterator = 'ModelInstanceIterator';
+    var $iterator = 'ModelInstanceManager';
 
     var $params;
     var $query;
@@ -435,6 +487,13 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
 
     function one() {
         $list = $this->limit(1)->all();
+        if (count($list) == 0)
+            throw new DoesNotExist();
+        elseif (count($list) > 1)
+            throw new ObjectNotUnique('One object was expected; however '
+                .'multiple objects in the database matched the query. '
+                .sprintf('In fact, there are %d matching objects.', count($list))
+            );
         // TODO: Throw error if more than one result from database
         return $this[0];
     }
@@ -513,23 +572,137 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
     }
 }
 
-class ModelInstanceIterator implements Iterator, ArrayAccess {
-    var $model;
+class DoesNotExist extends Exception {}
+class ObjectNotUnique extends Exception {}
+
+abstract class ResultSet implements Iterator, ArrayAccess {
     var $resource;
-    var $cache = array();
     var $position = 0;
     var $queryset;
-    var $map;
+    var $cache;
 
     function __construct($queryset=false) {
         $this->queryset = $queryset;
         if ($queryset) {
             $this->model = $queryset->model;
             $this->resource = $queryset->getQuery();
+        }
+    }
+
+    abstract function fillTo($index);
+
+    function asArray() {
+        $this->fillTo(PHP_INT_MAX);
+        return $this->cache;
+    }
+
+    // Iterator interface
+    function rewind() {
+        $this->position = 0;
+    }
+    function current() {
+        $this->fillTo($this->position);
+        return $this->cache[$this->position];
+    }
+    function key() {
+        return $this->position;
+    }
+    function next() {
+        $this->position++;
+    }
+    function valid() {
+        $this->fillTo($this->position);
+        return count($this->cache) > $this->position;
+    }
+
+    // ArrayAccess interface
+    function offsetExists($offset) {
+        $this->fillTo($offset);
+        return $this->position >= $offset;
+    }
+    function offsetGet($offset) {
+        $this->fillTo($offset);
+        return $this->cache[$offset];
+    }
+    function offsetUnset($a) {
+        throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
+    }
+    function offsetSet($a, $b) {
+        throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
+    }
+}
+
+class ModelInstanceManager extends ResultSet {
+    var $model;
+    var $map;
+
+    static $objectCache = array();
+
+    function __construct($queryset=false) {
+        parent::__construct($queryset);
+        if ($queryset) {
             $this->map = $this->resource->getMap();
         }
     }
 
+    function cache($model) {
+        $model::_inspect();
+        $key = sprintf('%s.%s',
+            $model::$meta->model, implode('.', $model->pk));
+        self::$objectCache[$key] = $model;
+    }
+
+    /**
+     * uncache
+     *
+     * Drop the cached reference to the model. If the model is deleted
+     * database-side. Lookups for the same model should not be short
+     * circuited to retrieve the cached reference.
+     */
+    static function uncache($model) {
+        $key = sprintf('%s.%s',
+            $model::$meta->model, implode('.', $model->pk));
+        unset(self::$objectCache[$key]);
+    }
+
+    static function checkCache($modelClass, $fields) {
+        $key = $modelClass::$meta->model;
+        foreach ($modelClass::$meta['pk'] as $f)
+            $key .= '.'.$fields[$f];
+        return @self::$objectCache[$key];
+    }
+
+    function getOrBuild($modelClass, $fields) {
+        // Check the cache for the model instance first
+        if ($m = self::checkCache($modelClass, $fields)) {
+            // TODO: If the model has deferred fields which are in $fields,
+            // those can be resolved here
+            return $m;
+        }
+        // Construct and cache the object
+        $this->cache($m = new $modelClass($fields));
+        $m->__deferred__ = $this->queryset->defer;
+        $m->__onload();
+        return $m;
+    }
+
+    /**
+     * buildModel
+     *
+     * This method builds the model including related models from the record
+     * received. For related recordsets, a $map should be setup inside this
+     * object prior to using this method. The $map is assumed to have this
+     * configuration:
+     *
+     * array(array(<modelClass>, <fieldCount>, <relativePath>, <alias>))
+     *
+     * Where $modelClass is the name of the foreign (with respect to the
+     * root model ($this->model), $fieldCount is the number of fields in the
+     * row for this model, $relativePath is the path that describes the
+     * relationship between the root model and this model, 'user__account'
+     * for instance, and <alias> is the alias for the model in the databse
+     * query — the field names should all start with <alias>_
+     */
     function buildModel($row) {
         // TODO: Traverse to foreign keys
         if ($this->map) {
@@ -542,7 +715,7 @@ class ModelInstanceIterator implements Iterator, ArrayAccess {
                 $fields = array_slice($row, $offset, $count);
                 if (!$path) {
                     // Build the root model
-                    $model = new $this->model($fields); # nolint
+                    $model = $this->getOrBuild($this->model, $fields);
                     $model->__onload();
                 }
                 else {
@@ -556,15 +729,13 @@ class ModelInstanceIterator implements Iterator, ArrayAccess {
                     foreach ($path as $field) {
                         $m = $m->get($field);
                     }
-                    $m->set($tail, new $model_class($fields));
+                    $m->set($tail, $this->getOrBuild($model_class, $fields));
                 }
                 $offset += $count;
             }
         }
         else {
-            $model = new $this->model($row); # nolint
-            $model->__deferred__ = $this->queryset->defer;
-            $model->__onload();
+            $model = $this->getOrBuild($this->model, $row);
         }
         return $model;
     }
@@ -580,49 +751,9 @@ class ModelInstanceIterator implements Iterator, ArrayAccess {
             }
         }
     }
-
-    function asArray() {
-        $this->fillTo(PHP_INT_MAX);
-        return $this->cache;
-    }
-
-    // Iterator interface
-    function rewind() {
-        $this->position = 0;
-    }
-    function current() {
-        $this->fillTo($this->position);
-        return $this->cache[$this->position];
-    }
-    function key() {
-        return $this->position;
-    }
-    function next() {
-        $this->position++;
-    }
-    function valid() {
-        $this->fillTo($this->position);
-        return count($this->cache) > $this->position;
-    }
-
-    // ArrayAccess interface
-    function offsetExists($offset) {
-        $this->fillTo($offset);
-        return $this->position >= $offset;
-    }
-    function offsetGet($offset) {
-        $this->fillTo($offset);
-        return $this->cache[$offset];
-    }
-    function offsetUnset($a) {
-        throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
-    }
-    function offsetSet($a, $b) {
-        throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
-    }
 }
 
-class FlatArrayIterator extends ModelInstanceIterator {
+class FlatArrayIterator extends ResultSet {
     function __construct($queryset) {
         $this->resource = $queryset->getQuery();
     }
@@ -639,7 +770,7 @@ class FlatArrayIterator extends ModelInstanceIterator {
     }
 }
 
-class InstrumentedList extends ModelInstanceIterator {
+class InstrumentedList extends ModelInstanceManager {
     var $key;
     var $id;
     var $model;
@@ -937,6 +1068,7 @@ class DbEngine {
     }
 
     static function delete(VerySimpleModel $model) {
+        ModelInstanceManager::uncache($model);
         return static::getCompiler()->compileDelete($model);
     }
 
@@ -966,6 +1098,7 @@ class MySqlCompiler extends SqlCompiler {
 
     function __contains($a, $b) {
         # {%a} like %{$b}%
+        # XXX: Escape $b
         return sprintf('%s LIKE %s', $a, $this->input($b = "%$b%"));
     }