From 8f4b37d9dc5a011894a8889798469b4e8a7e29dc Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Thu, 11 Jan 2007 00:43:08 +0000 Subject: [PATCH] Caching support from Zell. Hard drives the world round rejoice! --- .../jdbc/depot/CacheInvalidator.java | 35 ++ .../com/samskivert/jdbc/depot/CacheKey.java | 42 +++ .../jdbc/depot/ConstructedQuery.java | 302 ++++++++++++++++++ .../jdbc/depot/DepotMarshaller.java | 33 +- .../jdbc/depot/DepotRepository.java | 177 +++++++--- .../jdbc/depot/EntityMigration.java | 2 +- src/java/com/samskivert/jdbc/depot/Key.java | 188 ++++++++--- .../com/samskivert/jdbc/depot/Modifier.java | 81 ++++- .../com/samskivert/jdbc/depot/MultiKey.java | 149 +++++++++ .../jdbc/depot/PersistenceContext.java | 244 ++++++++++++-- src/java/com/samskivert/jdbc/depot/Query.java | 275 +--------------- .../samskivert/jdbc/depot/SimpleCacheKey.java | 113 +++++++ .../jdbc/depot/annotation/Computed.java | 4 +- .../jdbc/depot/clause/FieldOverride.java | 4 +- .../jdbc/depot/clause/ForUpdate.java | 4 +- .../jdbc/depot/clause/FromOverride.java | 4 +- .../samskivert/jdbc/depot/clause/GroupBy.java | 4 +- .../samskivert/jdbc/depot/clause/Join.java | 4 +- .../samskivert/jdbc/depot/clause/Limit.java | 4 +- .../samskivert/jdbc/depot/clause/OrderBy.java | 4 +- .../jdbc/depot/clause/QueryClause.java | 4 +- .../samskivert/jdbc/depot/clause/Where.java | 60 +++- .../jdbc/depot/expression/ColumnExp.java | 4 +- .../jdbc/depot/expression/FunctionExp.java | 4 +- .../jdbc/depot/expression/LiteralExp.java | 4 +- .../jdbc/depot/expression/SQLExpression.java | 4 +- .../jdbc/depot/expression/ValueExp.java | 4 +- .../jdbc/depot/operator/Conditionals.java | 8 +- .../samskivert/jdbc/depot/operator/Logic.java | 4 +- .../jdbc/depot/operator/SQLOperator.java | 6 +- 30 files changed, 1322 insertions(+), 453 deletions(-) create mode 100644 src/java/com/samskivert/jdbc/depot/CacheInvalidator.java create mode 100644 src/java/com/samskivert/jdbc/depot/CacheKey.java create mode 100644 src/java/com/samskivert/jdbc/depot/ConstructedQuery.java create mode 100644 src/java/com/samskivert/jdbc/depot/MultiKey.java create mode 100644 src/java/com/samskivert/jdbc/depot/SimpleCacheKey.java diff --git a/src/java/com/samskivert/jdbc/depot/CacheInvalidator.java b/src/java/com/samskivert/jdbc/depot/CacheInvalidator.java new file mode 100644 index 0000000..e6cd312 --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/CacheInvalidator.java @@ -0,0 +1,35 @@ +// +// $Id$ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2006 Michael Bayne, Pär Winzell +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.jdbc.depot; + +/** + * Implementors of this interface performs perform cache invalidation for calls to + * {@link DepotRepository#updateLiteral}, {@link DepotRepository#updatePartial} and + * {@link DepotRepository#deleteAll). + */ +public interface CacheInvalidator +{ + /** + * Must invalidate all cache entries that depend on the records being modified or deleted. + * This method is called just before the database statement is executed. + */ + public void invalidate (PersistenceContext ctx); +} diff --git a/src/java/com/samskivert/jdbc/depot/CacheKey.java b/src/java/com/samskivert/jdbc/depot/CacheKey.java new file mode 100644 index 0000000..671b838 --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/CacheKey.java @@ -0,0 +1,42 @@ +// +// $Id$ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2006 Michael Bayne, Pär Winzell +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.jdbc.depot; + +import java.io.Serializable; + +/** + * This interface uniquely identifies a single persistent entry for caching purposes. + * Queries that are given a {@link CacheKey} consult the cache before they hit the + * database. + */ +public interface CacheKey +{ + /** + * Returns the id of the cache in whose scope this key makes sense. + */ + public String getCacheId (); + + /** + * Returns the actual opaque serializable cache key under which results are stored + * in the cache identified by {@link #getCacheId}. + */ + public Serializable getCacheKey (); +} diff --git a/src/java/com/samskivert/jdbc/depot/ConstructedQuery.java b/src/java/com/samskivert/jdbc/depot/ConstructedQuery.java new file mode 100644 index 0000000..ee8061c --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/ConstructedQuery.java @@ -0,0 +1,302 @@ +// +// $Id$ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2006 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.jdbc.depot; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.samskivert.io.PersistenceException; +import com.samskivert.jdbc.depot.annotation.Computed; +import com.samskivert.jdbc.depot.clause.FieldOverride; +import com.samskivert.jdbc.depot.clause.ForUpdate; +import com.samskivert.jdbc.depot.clause.FromOverride; +import com.samskivert.jdbc.depot.clause.GroupBy; +import com.samskivert.jdbc.depot.clause.Join; +import com.samskivert.jdbc.depot.clause.Limit; +import com.samskivert.jdbc.depot.clause.OrderBy; +import com.samskivert.jdbc.depot.clause.QueryClause; +import com.samskivert.jdbc.depot.clause.Where; + +/** + * Encapsulates a non-modifying query of persistent objects. + */ +public abstract class ConstructedQuery + implements Query +{ + /** + * Maps a class referenced by this query to its associated table. + * + * This method is called by individual clauses. + */ + public String getTableName (Class cl) + { + return _classMap.get(cl).getTableName(); + } + + /** + * Maps a class referenced by this query to the abbreviation used for its associated table. The + * abbreviation is transientand only has meaning within the scope of this particular query. + * + * This method is called by individual clauses. + */ + public String getTableAbbreviation (Class cl) + { + int ix = _classList.indexOf(cl); + if (ix < 0) { + throw new IllegalArgumentException("Unknown persistence class: " + cl); + } + return "T" + (ix+1); + } + + // from Query + public CacheKey getCacheKey () + { + if (_where != null && _where instanceof CacheKey) { + return (CacheKey) _where; + } + return null; + } + + /** + * Translates the data in this Query object into a SQL query suited to instantiate a persistent + * object given the supplied key and clauses. These clauses are assumed to contain no + * conflicts. + * + * If no key is supplied all instances will be loaded. + */ + public PreparedStatement createQuery (Connection conn) + throws SQLException + { + if (_mainType == null) { // internal error + throw new RuntimeException("createQuery() called with _mainClass == null"); + } + + DepotMarshaller mainMarshaller = _classMap.get(_mainType); + String[] fields = mainMarshaller._allFields; + StringBuilder query = new StringBuilder("select "); + boolean skip = true; + for (int ii = 0; ii < fields.length; ii ++) { + if (!skip) { + query.append(", "); + } + skip = false; + + FieldOverride clause = _disMap.get(fields[ii]); + if (clause != null) { + clause.appendClause(this, query); + continue; + } + + Computed computed = mainMarshaller._fields.get(fields[ii]).getComputed(); + if (computed == null) { + // make sure the object corresponds to a table, otherwise the whole thing is + // computed + if (mainMarshaller.getTableName() != null) { + // if it's neither overridden nor computed, it's a standard field + query.append(getTableAbbreviation(_mainType)).append(".").append(fields[ii]); + continue; + } + throw new SQLException( + "@Computed entity field without definition [field=" + fields[ii] + "]"); + } + + // check if the computed field has a literal SQL definition + if (computed.fieldDefinition().length() > 0) { + query.append(computed.fieldDefinition() + " as " + fields[ii]); + + } else if (!computed.required()) { + // or if we can simply ignore the field + skip = true; + + } else { + throw new SQLException( + "@Computed(required) field without definition [field=" + fields[ii] + "]"); + } + } + + if (_fromOverride != null) { + _fromOverride.appendClause(this, query); + } else if (mainMarshaller.getTableName() != null) { + query.append(" from ").append(mainMarshaller.getTableName()); + query.append(" as ").append(getTableAbbreviation(_mainType)); + } else { + throw new SQLException("Query on @Computed entity with no FromOverrideClause."); + } + + for (Join clause : _joinClauses) { + clause.appendClause(this, query); + } + if (_where != null) { + _where.appendClause(this, query); + } + if (_groupBy != null) { + _groupBy.appendClause(this, query); + } + if (_orderBy != null) { + _orderBy.appendClause(this, query); + } + if (_limit != null) { + _limit.appendClause(this, query); + } + if (_forUpdate != null) { + _forUpdate.appendClause(this, query); + } + + PreparedStatement pstmt = conn.prepareStatement(query.toString()); + int argIdx = 1; + for (Join clause : _joinClauses) { + argIdx = clause.bindArguments(pstmt, argIdx); + } + if (_where != null) { + argIdx = _where.bindArguments(pstmt, argIdx); + } + if (_groupBy != null) { + argIdx = _groupBy.bindArguments(pstmt, argIdx); + } + if (_orderBy != null) { + argIdx = _orderBy.bindArguments(pstmt, argIdx); + } + if (_limit != null) { + argIdx = _limit.bindArguments(pstmt, argIdx); + } + if (_forUpdate != null) { + argIdx = _forUpdate.bindArguments(pstmt, argIdx); + } + + return pstmt; + } + + /** + * Creates a new Query object to generate one or more instances of the specified persistent + * class, as dictated by the key and query clauses. A persistence context is supplied for + * instantiation of marshallers, which may trigger table creations and schema migrations. + */ + protected ConstructedQuery (PersistenceContext ctx, Class type, QueryClause... clauses) + throws PersistenceException + { + _mainType = type; + Set classSet = new HashSet(); + if (type != null) { + classSet.add(type); + } + + for (QueryClause clause : clauses) { + if (clause instanceof Where) { + if (_where != null) { + throw new IllegalArgumentException( + "Query can't contain multiple Where clauses."); + } + _where = (Where) clause; + + } else if (clause instanceof FromOverride) { + if (_fromOverride != null) { + throw new IllegalArgumentException( + "Query can't contain multiple FromOverride clauses."); + } + _fromOverride = (FromOverride) clause; + classSet.addAll(_fromOverride.getClassSet()); + + } else if (clause instanceof Join) { + _joinClauses.add((Join) clause); + classSet.addAll(((Join) clause).getClassSet()); + + } else if (clause instanceof FieldOverride) { + _disMap.put(((FieldOverride) clause).getField(), + ((FieldOverride) clause)); + + } else if (clause instanceof OrderBy) { + if (_orderBy != null) { + throw new IllegalArgumentException( + "Query can't contain multiple OrderBy clauses."); + } + _orderBy = (OrderBy) clause; + + } else if (clause instanceof GroupBy) { + if (_groupBy != null) { + throw new IllegalArgumentException( + "Query can't contain multiple GroupBy clauses."); + } + _groupBy = (GroupBy) clause; + + } else if (clause instanceof Limit) { + if (_limit != null) { + throw new IllegalArgumentException( + "Query can't contain multiple Limit clauses."); + } + _limit = (Limit) clause; + + } else if (clause instanceof ForUpdate) { + if (_forUpdate != null) { + throw new IllegalArgumentException( + "Query can't contain multiple For Update clauses."); + } + _forUpdate = (ForUpdate) clause; + } + } + _classMap = new HashMap(); + + for (Class c : classSet) { + _classMap.put(c, ctx.getMarshaller(c)); + } + _classList = new ArrayList(classSet); + } + + /** The persistent class to instantiate for the results. */ + protected Class _mainType; + + /** A list of referenced classes, used to generate table abbreviations. */ + protected List _classList; + + /** Classes mapped to marshallers, used for table names and field lists. */ + protected Map _classMap; + + /** Persistent class fields mapped to field override clauses. */ + protected Map _disMap = new HashMap(); + + /** The from override clause, if any. */ + protected FromOverride _fromOverride; + + /** The where clause. */ + protected Where _where; + + /** A list of join clauses, each potentially referencing a new class. */ + protected List _joinClauses = new ArrayList(); + + /** The order by clause, if any. */ + protected OrderBy _orderBy; + + /** The group by clause, if any. */ + protected GroupBy _groupBy; + + /** The limit clause, if any. */ + protected Limit _limit; + + /** The For Update clause, if any. */ + protected ForUpdate _forUpdate; +} diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index b710d54..431fba2 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -47,7 +47,6 @@ import com.samskivert.jdbc.depot.annotation.Transient; import com.samskivert.jdbc.JDBCUtil; import com.samskivert.jdbc.depot.clause.Where; -import com.samskivert.jdbc.depot.expression.ColumnExp; import com.samskivert.util.ArrayUtil; import com.samskivert.util.ListUtil; import com.samskivert.util.StringUtil; @@ -255,11 +254,11 @@ public class DepotMarshaller * Returns a key configured with the primary key of the supplied object. Throws an exception * if the persistent object did not declare a primary key. */ - public Key getPrimaryKey (Object object) + public Key getPrimaryKey (Object object) { if (!hasPrimaryKey()) { throw new UnsupportedOperationException( - getClass().getName() + " does not define a primary key"); + _pclass.getName() + " does not define a primary key"); } try { Comparable[] values = new Comparable[_pkColumns.size()]; @@ -277,23 +276,17 @@ public class DepotMarshaller * Creates a primary key record for the type of object handled by this marshaller, using the * supplied primary key value. */ - public Key makePrimaryKey (Comparable... values) + public Key makePrimaryKey (Comparable... values) { if (!hasPrimaryKey()) { throw new UnsupportedOperationException( getClass().getName() + " does not define a primary key"); } - if (values.length != _pkColumns.size()) { - throw new IllegalArgumentException( - "Argument count (" + values.length + ") must match primary key size (" + - _pkColumns.size() + ")"); - } - ColumnExp[] columns = new ColumnExp[_pkColumns.size()]; + String[] columns = new String[_pkColumns.size()]; for (int ii = 0; ii < _pkColumns.size(); ii++) { - FieldMarshaller field = _pkColumns.get(ii); - columns[ii] = new ColumnExp(_pclass, field.getColumnName()); + columns[ii] = _pkColumns.get(ii).getColumnName(); } - return new Key(columns, values); + return new Key(_pclass, columns, values); } /** @@ -552,7 +545,7 @@ public class DepotMarshaller /** * Initializes the table used by this marshaller. This is called automatically by the {@link - * PersistenceContext} the first time an entity is used. If the table does not exist, it will + * PersistenceContext} the first time an entity is used. If the table does not exist, it will * be created. If the schema version specified by the persistent object is newer than the * database schema, it will be migrated. */ @@ -571,7 +564,7 @@ public class DepotMarshaller } // check to see if our schema version table exists, create it if not - ctx.invoke(new Modifier(null) { + ctx.invoke(new Modifier() { public int invoke (Connection conn) throws SQLException { JDBCUtil.createTableIfMissing( conn, SCHEMA_VERSION_TABLE, @@ -582,7 +575,7 @@ public class DepotMarshaller }); // now create the table for our persistent class if it does not exist - ctx.invoke(new Modifier(null) { + ctx.invoke(new Modifier() { public int invoke (Connection conn) throws SQLException { if (!JDBCUtil.tableExists(conn, getTableName())) { log.info("Creating table " + getTableName() + " (" + _declarations + ") " + @@ -597,7 +590,7 @@ public class DepotMarshaller // if we have a key generator, initialize that too if (_keyGenerator != null) { - ctx.invoke(new Modifier(null) { + ctx.invoke(new Modifier() { public int invoke (Connection conn) throws SQLException { _keyGenerator.init(conn); return 0; @@ -611,7 +604,7 @@ public class DepotMarshaller } // make sure the versions match - int currentVersion = ctx.invoke(new Modifier(null) { + int currentVersion = ctx.invoke(new Modifier() { public int invoke (Connection conn) throws SQLException { String query = "select version from " + SCHEMA_VERSION_TABLE + " where persistentClass = '" + getTableName() + "'"; @@ -643,7 +636,7 @@ public class DepotMarshaller // enumerate all of the columns now that we've run our pre-migrations final HashSet columns = new HashSet(); final HashSet indices = new HashSet(); - ctx.invoke(new Modifier(null) { + ctx.invoke(new Modifier() { public int invoke (Connection conn) throws SQLException { DatabaseMetaData meta = conn.getMetaData(); ResultSet rs = meta.getColumns(null, null, getTableName(), "%"); @@ -730,7 +723,7 @@ public class DepotMarshaller } // record our new version in the database - ctx.invoke(new Modifier(null) { + ctx.invoke(new Modifier() { public int invoke (Connection conn) throws SQLException { updateVersion(conn, _schemaVersion); return 0; diff --git a/src/java/com/samskivert/jdbc/depot/DepotRepository.java b/src/java/com/samskivert/jdbc/depot/DepotRepository.java index f099876..a56e95d 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotRepository.java +++ b/src/java/com/samskivert/jdbc/depot/DepotRepository.java @@ -29,8 +29,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Map; +import net.sf.ehcache.Cache; +import net.sf.ehcache.Element; + import com.samskivert.io.PersistenceException; import com.samskivert.jdbc.ConnectionProvider; +import com.samskivert.jdbc.JDBCUtil; +import com.samskivert.jdbc.depot.Modifier.*; import com.samskivert.jdbc.depot.clause.QueryClause; import com.samskivert.jdbc.depot.clause.Where; import com.samskivert.util.ArrayUtil; @@ -68,6 +73,38 @@ public class DepotRepository return load(type, clauses); } + /** + * Loads the persistent object that matches the specified primary key. + */ + protected T load (Class type, String ix, Comparable val, QueryClause... clauses) + throws PersistenceException + { + clauses = ArrayUtil.append(clauses, new Key(type, ix, val)); + return load(type, clauses); + } + + /** + * Loads the persistent object that matches the specified two-column primary key. + */ + protected T load (Class type, String ix1, Comparable val1, String ix2, + Comparable val2, QueryClause... clauses) + throws PersistenceException + { + clauses = ArrayUtil.append(clauses, new Key(type, ix1, val1, ix2, val2)); + return load(type, clauses); + } + + /** + * Loads the persistent object that matches the specified three-column primary key. + */ + protected T load (Class type, String ix1, Comparable val1, String ix2, + Comparable val2, String ix3, Comparable val3, QueryClause... clauses) + throws PersistenceException + { + clauses = ArrayUtil.append(clauses, new Key(type, ix1, val1, ix2, val2, ix3, val3)); + return load(type, clauses); + } + /** * Loads the first persistent object that matches the supplied key. */ @@ -75,7 +112,7 @@ public class DepotRepository throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(type); - return _ctx.invoke(new Query(_ctx, type, clauses) { + return _ctx.invoke(new ConstructedQuery(_ctx, type, clauses) { public T invoke (Connection conn) throws SQLException { PreparedStatement stmt = createQuery(conn); try { @@ -89,9 +126,22 @@ public class DepotRepository return result; } finally { - stmt.close(); + JDBCUtil.close(stmt); } } + + public void updateCache (PersistenceContext ctx, T result) { + CacheKey key = getCacheKey(); + if (key == null) { + // no row-specific cache key was given + if (result == null || !marsh.hasPrimaryKey()) { + return; + } + // if we can, create a key from what was actually returned + key = marsh.getPrimaryKey(result); + } + ctx.cacheStore(key, result); + } }); } @@ -103,7 +153,7 @@ public class DepotRepository throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(type); - return _ctx.invoke(new Query>(_ctx, type, clauses) { + return _ctx.invoke(new ConstructedQuery>(_ctx, type, clauses) { public ArrayList invoke (Connection conn) throws SQLException { PreparedStatement stmt = createQuery(conn); try { @@ -115,7 +165,15 @@ public class DepotRepository return results; } finally { - stmt.close(); + JDBCUtil.close(stmt); + } + } + + public void updateCache (PersistenceContext ctx, ArrayList result) { + if (marsh.hasPrimaryKey()) { + for (T bit : result) { + ctx.cacheStore(marsh.getPrimaryKey(bit), bit); + } } } }); @@ -127,11 +185,11 @@ public class DepotRepository * * @return the number of rows modified by this action, this should always be one. */ - protected int insert (final Object record) + protected int insert (final T record) throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(record.getClass()); - return _ctx.invoke(new Modifier(null) { + return _ctx.invoke(new CachingModifier(null, null) { public int invoke (Connection conn) throws SQLException { // update our modifier's key so that it can cache our results updateKey(marsh.assignPrimaryKey(conn, record, false)); @@ -140,9 +198,10 @@ public class DepotRepository int mods = stmt.executeUpdate(); // check again in case we have a post-factum key generator updateKey(marsh.assignPrimaryKey(conn, record, true)); + setInstance(record); return mods; } finally { - stmt.close(); + JDBCUtil.close(stmt); } } }); @@ -154,17 +213,18 @@ public class DepotRepository * * @return the number of rows modified by this action. */ - protected int update (final Object record) + protected int update (final T record) throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(record.getClass()); - return _ctx.invoke(new Modifier(marsh.getPrimaryKey(record)) { + final Key key = marsh.getPrimaryKey(record); + return _ctx.invoke(new CachingModifier(key, key) { public int invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createUpdate(conn, record, _key); + PreparedStatement stmt = marsh.createUpdate(conn, record, key); try { return stmt.executeUpdate(); } finally { - stmt.close(); + JDBCUtil.close(stmt); } } }); @@ -176,17 +236,18 @@ public class DepotRepository * * @return the number of rows modified by this action. */ - protected int update (final Object record, final String... modifiedFields) + protected int update (final T record, final String... modifiedFields) throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(record.getClass()); - return _ctx.invoke(new Modifier(marsh.getPrimaryKey(record)) { + final Key key = marsh.getPrimaryKey(record); + return _ctx.invoke(new CachingModifier(key, key) { public int invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createUpdate(conn, record, _key, modifiedFields); + PreparedStatement stmt = marsh.createUpdate(conn, record, key, modifiedFields); try { return stmt.executeUpdate(); } finally { - stmt.close(); + JDBCUtil.close(stmt); } } }); @@ -228,8 +289,8 @@ public class DepotRepository protected int updatePartial (Class type, Comparable primaryKey, Object... fieldsValues) throws PersistenceException { - return updatePartial( - type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey), fieldsValues); + Key key = _ctx.getMarshaller(type).makePrimaryKey(primaryKey); + return updatePartial(type, key, key, fieldsValues); } /** @@ -237,12 +298,15 @@ public class DepotRepository * * @param type the type of the persistent object to be modified. * @param key the key to match in the update. + * @param invalidator a cache invalidator that will be prior to the update to flush the + * relevant persistent objects from the cache. * @param fieldsValues an array containing the names of the fields/columns and the values to be * assigned, in key, value, key, value, etc. order. * * @return the number of rows modified by this action. */ - protected int updatePartial (Class type, Where key, Object... fieldsValues) + protected int updatePartial (Class type, final Where key, CacheInvalidator invalidator, + Object... fieldsValues) throws PersistenceException { // separate the arguments into keys and values @@ -254,13 +318,13 @@ public class DepotRepository } final DepotMarshaller marsh = _ctx.getMarshaller(type); - return _ctx.invoke(new Modifier(key) { + return _ctx.invoke(new Modifier(invalidator) { public int invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createPartialUpdate(conn, _key, fields, values); + PreparedStatement stmt = marsh.createPartialUpdate(conn, key, fields, values); try { return stmt.executeUpdate(); } finally { - stmt.close(); + JDBCUtil.close(stmt); } } }); @@ -268,8 +332,8 @@ public class DepotRepository /** * Updates the specified columns for all persistent objects matching the supplied primary - * key. The values in this case must be literal SQL to be inserted into the update - * statement. In general this is used when you want to do something like the following: + * key. The values in this case must be literal SQL to be inserted into the update statement. + * In general this is used when you want to do something like the following: * *
      * update FOO set BAR = BAR + 1;
@@ -286,14 +350,14 @@ public class DepotRepository
     protected  int updateLiteral (Class type, Comparable primaryKey, String... fieldsValues)
         throws PersistenceException
     {
-        return updateLiteral(
-            type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey), fieldsValues);
+        Key key = _ctx.getMarshaller(type).makePrimaryKey(primaryKey);
+        return updateLiteral(type, key, key, fieldsValues);
     }
 
     /**
      * Updates the specified columns for all persistent objects matching the supplied primary
-     * key. The values in this case must be literal SQL to be inserted into the update
-     * statement. In general this is used when you want to do something like the following:
+     * key. The values in this case must be literal SQL to be inserted into the update statement.
+     * In general this is used when you want to do something like the following:
      *
      * 
      * update FOO set BAR = BAR + 1;
@@ -307,7 +371,8 @@ public class DepotRepository
      *
      * @return the number of rows modified by this action.
      */
-    protected  int updateLiteral (Class type, Where key, String... fieldsValues)
+    protected  int updateLiteral (Class type, final Where key, CacheInvalidator invalidator,
+                                     String... fieldsValues)
         throws PersistenceException
     {
         // separate the arguments into keys and values
@@ -319,13 +384,13 @@ public class DepotRepository
         }
 
         final DepotMarshaller marsh = _ctx.getMarshaller(type);
-        return _ctx.invoke(new Modifier(key) {
+        return _ctx.invoke(new Modifier(invalidator) {
             public int invoke (Connection conn) throws SQLException {
-                PreparedStatement stmt = marsh.createLiteralUpdate(conn, _key, fields, values);
+                PreparedStatement stmt = marsh.createLiteralUpdate(conn, key, fields, values);
                 try {
                     return stmt.executeUpdate();
                 } finally {
-                    stmt.close();
+                    JDBCUtil.close(stmt);
                 }
             }
         });
@@ -338,24 +403,24 @@ public class DepotRepository
      *
      * @return the number of rows modified by this action, this should always be one.
      */
-    protected int store (final Object record)
+    protected  int store (final T record)
         throws PersistenceException
     {
         final DepotMarshaller marsh = _ctx.getMarshaller(record.getClass());
-        Key key = marsh.hasPrimaryKey() ? marsh.getPrimaryKey(record) : null;
-        return _ctx.invoke(new Modifier(key) {
+        final Key key = marsh.hasPrimaryKey() ? marsh.getPrimaryKey(record) : null;
+        return _ctx.invoke(new CachingModifier(key, key) {
             public int invoke (Connection conn) throws SQLException {
                 PreparedStatement stmt = null;
                 try {
                     // if our primary key is null or is the integer 0, assume the record has never
                     // before been persisted and insert
-                    if (_key != null && !Integer.valueOf(0).equals(_key)) {
-                        stmt = marsh.createUpdate(conn, record, _key);
+                    if (key != null && !Integer.valueOf(0).equals(key)) {
+                        stmt = marsh.createUpdate(conn, record, key);
                         int mods = stmt.executeUpdate();
                         if (mods > 0) {
                             return mods;
                         }
-                        stmt.close();
+                        JDBCUtil.close(stmt);
                     }
 
                     // if the update modified zero rows or the primary key was obviously unset, do
@@ -364,10 +429,11 @@ public class DepotRepository
                     stmt = marsh.createInsert(conn, record);
                     int mods = stmt.executeUpdate();
                     updateKey(marsh.assignPrimaryKey(conn, record, true));
+                    setInstance(record);
                     return mods;
 
                 } finally {
-                    stmt.close();
+                    JDBCUtil.close(stmt);
                 }
             }
         });
@@ -383,8 +449,8 @@ public class DepotRepository
         throws PersistenceException
     {
         @SuppressWarnings("unchecked") Class type = (Class)record.getClass();
-        DepotMarshaller marsh = _ctx.getMarshaller(type);
-        return deleteAll(type, marsh.getPrimaryKey(record));
+        Key primaryKey = _ctx.getMarshaller(type).getPrimaryKey(record);
+        return deleteAll(type, primaryKey, primaryKey);
     }
 
     /**
@@ -393,10 +459,11 @@ public class DepotRepository
      *
      * @return the number of rows deleted by this action.
      */
-    protected  int delete (Class type, Comparable primaryKey)
+    protected  int delete (Class type, Comparable primaryKeyValue)
         throws PersistenceException
     {
-        return deleteAll(type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey));
+        Key primaryKey = _ctx.getMarshaller(type).makePrimaryKey(primaryKeyValue);
+        return deleteAll(type, primaryKey, primaryKey);
     }
 
     /**
@@ -404,31 +471,41 @@ public class DepotRepository
      *
      * @return the number of rows deleted by this action.
      */
-    protected  int deleteAll (Class type, Where key)
+    protected  int deleteAll (Class type, final Where key, CacheInvalidator invalidator)
         throws PersistenceException
     {
         final DepotMarshaller marsh = _ctx.getMarshaller(type);
-        return _ctx.invoke(new Modifier(key) {
+        return _ctx.invoke(new Modifier(invalidator) {
             public int invoke (Connection conn) throws SQLException {
-                PreparedStatement stmt = marsh.createDelete(conn, _key);
+                PreparedStatement stmt = marsh.createDelete(conn, key);
                 try {
                     return stmt.executeUpdate();
                 } finally {
-                    stmt.close();
+                    JDBCUtil.close(stmt);
                 }
             }
         });
     }
 
-    protected static abstract class CollectionQuery extends Query
+    protected static abstract class CollectionQuery implements Query
     {
-        public CollectionQuery (PersistenceContext ctx, Class type, Key key)
+        public CollectionQuery (CacheKey key)
             throws PersistenceException
         {
-            super(ctx, type, key);
+            _key = key;
         }
 
-        public abstract T invoke (Connection conn) throws SQLException;
+        public CacheKey getCacheKey ()
+        {
+            return _key;
+        }
+
+        public void updateCache (PersistenceContext ctx, T result)
+        {
+            ctx.cacheStore(_key, result);
+        }
+
+        protected CacheKey _key;
     }
 
     protected PersistenceContext _ctx;
diff --git a/src/java/com/samskivert/jdbc/depot/EntityMigration.java b/src/java/com/samskivert/jdbc/depot/EntityMigration.java
index d5ec6cc..a1400b5 100644
--- a/src/java/com/samskivert/jdbc/depot/EntityMigration.java
+++ b/src/java/com/samskivert/jdbc/depot/EntityMigration.java
@@ -113,7 +113,7 @@ public abstract class EntityMigration extends Modifier
 
     protected EntityMigration (int targetVersion)
     {
-        super(null);
+        super();
         _targetVersion = targetVersion;
     }
 
diff --git a/src/java/com/samskivert/jdbc/depot/Key.java b/src/java/com/samskivert/jdbc/depot/Key.java
index 315d8fd..7f63490 100644
--- a/src/java/com/samskivert/jdbc/depot/Key.java
+++ b/src/java/com/samskivert/jdbc/depot/Key.java
@@ -20,71 +20,161 @@
 
 package com.samskivert.jdbc.depot;
 
+import java.io.Serializable;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 import com.samskivert.jdbc.depot.clause.Where;
-import com.samskivert.jdbc.depot.expression.ColumnExp;
-import com.samskivert.jdbc.depot.expression.LiteralExp;
-import com.samskivert.jdbc.depot.expression.ValueExp;
-import com.samskivert.jdbc.depot.operator.Conditionals.Equals;
-import com.samskivert.jdbc.depot.operator.Conditionals.IsNull;
-import com.samskivert.jdbc.depot.operator.Logic.And;
-import com.samskivert.jdbc.depot.operator.SQLOperator;
 
 /**
- * Encapsulates a key used to match persistent objects in a query. This is a special form of
- * where clause that is meant to uniquely identify a specific row for caching purposes. It is
- * generated by the update/insert code, as well as instantiated directly by users.
+ * A special form of {@link Where} clause that uniquely specifices a single database row and
+ * thus also a single persistent object. Because it implements both {@link CacheKey} and
+ * {@link CacheInvalidator} it also uniquely indexes into the cache and knows how to invalidate
+ * itself upon modification. This class is created by many {@link DepotMarshaller} methods as
+ * a convenience, and may also be instantiated explicitly.
  */
-public class Key extends Where
+public class Key extends Where
+    implements CacheKey, CacheInvalidator
 {
-    public Key (String index, Comparable value)
+    /**
+     * Constructs a new single-column {@code Key} with the given value. 
+     */
+    public Key (Class pClass, String ix, Comparable val)
     {
-        this(new ColumnExp(index), value);
+        this(pClass, new String[] { ix }, new Comparable[] { val });
     }
 
-    public Key (ColumnExp column, Comparable value)
+    /**
+     * Constructs a new two-column {@code Key} with the given values.
+     */
+    public Key (Class pClass, String ix1, Comparable val1,
+                String ix2, Comparable val2)
     {
-        this(new ColumnExp[] { column }, new Comparable[] { value });
+        this(pClass, new String[] { ix1, ix2 }, new Comparable[] { val1, val2 });
     }
 
-    public Key (ColumnExp index1, Comparable value1,
-                ColumnExp index2, Comparable value2)
+    /**
+     * Constructs a new three-column {@code Key} with the given values.
+     */
+    public Key (Class pClass, String ix1, Comparable val1,
+                String ix2, Comparable val2, String ix3, Comparable val3)
     {
-        this(new ColumnExp[] { index1, index2 },
-             new Comparable[] { value1, value2 });
+        this(pClass, new String[] { ix1, ix2, ix3 }, new Comparable[] { val1, val2, val3 });
     }
 
-    public Key (ColumnExp index1, Comparable value1,
-                ColumnExp index2, Comparable value2,
-                ColumnExp index3, Comparable value3)
+    /**
+     * Constructs a new multi-column {@code Key} with the given values.
+     * 
+     * TODO: There is no reason to store both fields and values here (and doing so probably more
+     * than doubles the space the key consumes in the cache). The primary key fields are known
+     * by the DepotMarshaller; we should simply check tha the given indices match those, and
+     * store the values in the order defined by the DepotMarshaller. The sanity check would be
+     * welcome in any case. The only problem with this is that we don't currently have access to
+     * a {@link PersistenceContext}. It may be that we should make this class internal to
+     * {@link DepotRepository} and only create it ourselves. This would require *lots* more
+     * convenience methods in {@link DepotRepository} though.
+     */
+    public Key (Class pClass, String[] fields, Comparable[] values)
     {
-        this(new ColumnExp[] { index1, index2, index3 },
-             new Comparable[] { value1, value2, value3 });
-    }
+        // TODO: make Where an interface so we don't have to do this ugly super call
+        super(null);
 
-    public Key (String index1, Comparable value1, String index2, Comparable value2)
-    {
-        this(new ColumnExp(index1), value1, new ColumnExp(index2), value2);
-    }
-
-    public Key (String index1, Comparable value1, String index2, Comparable value2,
-                String index3, Comparable value3)
-    {
-        this(new ColumnExp(index1), value1, new ColumnExp(index2), value2, 
-             new ColumnExp(index3), value3);
-    }
-
-    public Key (ColumnExp[] columns, Comparable[] values)
-    {
-        super(toCondition(columns, values));
-    }
-
-    protected static SQLOperator toCondition (ColumnExp[] columns, Comparable[] values)
-    {
-        SQLOperator[] comparisons = new SQLOperator[columns.length];
-        for (int ii = 0; ii < columns.length; ii ++) {
-            comparisons[ii] = (values[ii] == null) ?
-                new IsNull(columns[ii]) : new Equals(columns[ii], new ValueExp(values[ii]));
+        if (fields.length != values.length) {
+            throw new IllegalArgumentException("Field and Value arrays must be of equal length.");
+        }
+        _pClass = pClass;
+        _map = new HashMap();
+        for (int i = 0; i < fields.length; i ++) {
+            _map.put(fields[i], values[i]);
         }
-        return new And(comparisons);
     }
+
+    // from QueryClause
+    public List getClassSet()
+    {
+        return Arrays.asList(new Class[] { _pClass });
+    }
+
+    // from QueryClause
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
+    {
+        builder.append(" where ");
+        boolean first = true;
+        for (Map.Entry entry : _map.entrySet()) {
+            if (first) {
+                first = false;
+            } else {
+                builder.append(" and ");
+            }
+            builder.append(entry.getKey());
+            builder.append(entry.getValue() == null ? " is null " : " = ? ");
+        }
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        for (Map.Entry entry : _map.entrySet()) {
+            if (entry.getValue() != null) {
+                pstmt.setObject(argIdx ++, entry.getValue());
+            }
+        }
+        return argIdx;
+    }
+    
+    // from CacheKey
+    public String getCacheId ()
+    {
+        return _pClass.getName();
+    }
+
+    // from CacheKey
+    public Serializable getCacheKey ()
+    {
+        return _map;
+    }
+
+    // from CacheInvalidator
+    public void invalidate (PersistenceContext ctx)
+    {
+        ctx.cacheInvalidate(this);
+    }
+
+    @Override
+    public int hashCode ()
+    {
+        return _pClass.hashCode() + 31 * _map.hashCode();
+    }
+
+    @Override
+    public boolean equals (Object obj)
+    {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        Key other = (Key) obj;
+        return (_pClass == other._pClass && _map.equals(other._map));
+    }
+
+    @Override
+    public String toString ()
+    {
+        StringBuilder builder = new StringBuilder("[Key pClass=" + _pClass.getName());
+        for (Map.Entry entry : _map.entrySet()) {
+            builder.append(", ").append(entry.getKey()).append("=").append(entry.getValue());
+        }
+        builder.append("]");
+        return builder.toString();
+    }
+    
+    protected Class _pClass;
+    protected HashMap _map;
 }
diff --git a/src/java/com/samskivert/jdbc/depot/Modifier.java b/src/java/com/samskivert/jdbc/depot/Modifier.java
index a5b208e..632274f 100644
--- a/src/java/com/samskivert/jdbc/depot/Modifier.java
+++ b/src/java/com/samskivert/jdbc/depot/Modifier.java
@@ -3,7 +3,7 @@
 //
 // samskivert library - useful routines for java programs
 // Copyright (C) 2006 Michael Bayne
-// 
+//
 // This library is free software; you can redistribute it and/or modify it
 // under the terms of the GNU Lesser General Public License as published
 // by the Free Software Foundation; either version 2.1 of the License, or
@@ -24,15 +24,15 @@ import java.sql.Connection;
 import java.sql.SQLException;
 import java.sql.Statement;
 
-import com.samskivert.jdbc.depot.clause.Where;
-
 /**
  * Encapsulates a modification of persistent objects.
  */
 public abstract class Modifier
 {
-    /** A simple modifier that executes a single SQL statement. No cache flushing is done as a
-     * result of this operation. */
+    /**
+     * A simple modifier that executes a single SQL statement. No cache flushing is done as a
+     * result of this operation.
+     */
     public static class Simple extends Modifier
     {
         public Simple (String query) {
@@ -52,19 +52,74 @@ public abstract class Modifier
         protected String _query;
     }
 
-    public abstract int invoke (Connection conn) throws SQLException;
-
-    protected Modifier (Where key)
+    /**
+     * A simple modifier that updates the cache with its modified object on completion. The derived
+     * class must call {@link #setInstance} to inform the modifier of its object at some point
+     * during the modification operation.
+     */
+    public static abstract class CachingModifier extends Modifier
     {
-        _key = key;
+        protected CachingModifier (CacheKey key, CacheInvalidator invalidator)
+        {
+            super(invalidator);
+            _key = key;
+        }
+
+        protected void setInstance (T result)
+        {
+            _result = result;
+        }
+
+        protected void updateKey (CacheKey key)
+        {
+            if (key != null) {
+                _key = key;
+            }
+        }
+
+        @Override
+        public void cacheUpdate (PersistenceContext ctx)
+        {
+            super.cacheUpdate(ctx);
+            if (_key != null) {
+                ctx.cacheStore(_key, _result);
+            }
+        }
+
+        protected CacheKey _key;
+        protected T _result;
     }
 
-    protected void updateKey (Key key)
+    public abstract int invoke (Connection conn) throws SQLException;
+
+    public Modifier ()
     {
-        if (key != null) {
-            _key = key;
+        this(null);
+    }
+
+    public Modifier (CacheInvalidator invalidator)
+    {
+        _invalidator = invalidator;
+    }
+
+    /**
+     * Do any cache invalidation needed for this modification. This method is called just
+     * before the database statement is executed.
+     */
+    public void cacheInvalidation (PersistenceContext ctx)
+    {
+        if (_invalidator != null) {
+            _invalidator.invalidate(ctx);
         }
     }
 
-    protected Where _key;
+    /**
+     * Do any cache updates needed for this modification. This method is called just after
+     * the successful execution of the database statement.
+     */
+    public void cacheUpdate (PersistenceContext ctx)
+    {
+    }
+
+    protected CacheInvalidator _invalidator;
 }
diff --git a/src/java/com/samskivert/jdbc/depot/MultiKey.java b/src/java/com/samskivert/jdbc/depot/MultiKey.java
new file mode 100644
index 0000000..0176d84
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/MultiKey.java
@@ -0,0 +1,149 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.samskivert.jdbc.depot.clause.Where;
+
+/**
+ * A special form of {@link Where} clause that specifies an explicit range of database rows. It
+ * does not implement {@link CacheKey} but it does implement {@link CacheInvalidator} which means
+ * it can be sent into e.g. {@link DepotRepository#deleteAll) and have it clean up after itself.
+ */
+public class MultiKey extends Where
+    implements CacheInvalidator
+{
+    /**
+     * Constructs a new single-column {@code MultiKey} with the given value range.
+     */
+    public MultiKey (Class pClass, String ix, Comparable... val)
+    {
+        this(pClass, new String[0], new Comparable[0], ix, val);
+    }
+
+    /**
+     * Constructs a new two-column {@code MultiKey} with the given value range.
+     */
+    public MultiKey (Class pClass, String ix1, Comparable val1, String ix2, Comparable... val2)
+    {
+        this(pClass, new String[] { ix1 }, new Comparable[] { val1 }, ix2, val2);
+    }
+
+    /**
+     * Constructs a new three-column {@code MultiKey} with the given value range.
+     */
+    public MultiKey (Class pClass, String ix1, Comparable val1, String ix2, Comparable val2,
+                     String ix3, Comparable... val3)
+    {
+        this(pClass, new String[] { ix1, ix2 }, new Comparable[] { val1, val2 }, ix3, val3);
+    }
+
+    /**
+     * Constructs a new multi-column {@code MultiKey} with the given value range.
+     * @TODO: See {@link Key#Key(Class, String[], Comparable[]) for somewhat relevant comments.
+     */
+    public MultiKey (Class pClass, String[] sFields, Comparable[] sValues,
+                     String mField, Comparable[] mValues)
+    {
+        // TODO
+        super(null);
+        if (sFields.length != sValues.length) {
+            throw new IllegalArgumentException(
+                "Key field and values arrays must be of equal length.");
+        }
+        _pClass = pClass;
+        _mField = mField;
+        _mValues = mValues;
+        _map = new HashMap();
+        for (int i = 0; i < sFields.length; i ++) {
+            _map.put(sFields[i], sValues[i]);
+        }
+    }
+
+    // from QueryClause
+    public List getClassSet()
+    {
+        return Arrays.asList(new Class[] { _pClass });
+    }
+
+    // from QueryClause
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
+    {
+        builder.append(" where ");
+        boolean first = true;
+        for (Map.Entry entry : _map.entrySet()) {
+            if (first) {
+                first = false;
+            } else {
+                builder.append(" and ");
+            }
+            builder.append(entry.getKey());
+            builder.append(entry.getValue() == null ? " is null " : " = ? ");
+        }
+        if (!first) {
+            builder.append(" and ");
+        }
+        builder.append(_mField).append(" in (");
+        for (int ii = 0; ii < _mValues.length; ii ++) {
+            if (ii > 0) {
+                builder.append(", ");
+            }
+            builder.append("?");
+        }
+        builder.append(")");
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        for (Map.Entry entry : _map.entrySet()) {
+            if (entry.getValue() != null) {
+                pstmt.setObject(argIdx ++, entry.getValue());
+            }
+        }
+        for (int ii = 0; ii < _mValues.length; ii++) {
+            pstmt.setObject(argIdx ++, _mValues[ii]);
+        }
+        return argIdx;
+    }
+
+    // from CacheInvalidator
+    public void invalidate (PersistenceContext ctx)
+    {
+        HashMap newMap = new HashMap(_map);
+        for (int i = 0; i < _mValues.length; i ++) {
+            newMap.put(_mField, _mValues[i]);
+            ctx.cacheInvalidate(new SimpleCacheKey(_pClass, newMap));
+        }
+    }
+
+    protected Class _pClass;
+    protected HashMap _map;
+    protected String _mField;
+    protected Comparable[] _mValues;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/PersistenceContext.java b/src/java/com/samskivert/jdbc/depot/PersistenceContext.java
index 94468ef..92b56fc 100644
--- a/src/java/com/samskivert/jdbc/depot/PersistenceContext.java
+++ b/src/java/com/samskivert/jdbc/depot/PersistenceContext.java
@@ -21,12 +21,20 @@
 package com.samskivert.jdbc.depot;
 
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 import com.samskivert.jdbc.depot.annotation.TableGenerator;
 
+import java.io.Serializable;
 import java.sql.Connection;
 import java.sql.SQLException;
 
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+
 import com.samskivert.io.PersistenceException;
 
 import com.samskivert.util.StringUtil;
@@ -44,16 +52,66 @@ public class PersistenceContext
     public HashMap tableGenerators = new HashMap();
 
     /**
-     * Creates a persistence context that will use the supplied provider to
-     * obtain JDBC connections.
+     * A cache listener is notified when cache entries are invalited through creation, deletion,
+     * or modification. Its purpose is typically to do further invalidation of dependent entries
+     * in other caches.
+     */
+    public static interface CacheListener
+    {
+        /**
+         * The given entry has just been deleted, modified or created. Do what thou wilt.
+         */
+        public void entryModified (CacheKey key, T entry);
+    }
+
+    /**
+     * The callback for {@link #cacheTraverse}; this is called for each entry in a given cache.
+     */
+    public static interface CacheTraverser
+    {
+        /**
+         * Performs whatever cache-related tasks need doing for this cache entry. This method
+         * is called for each cache entry in a full-cache enumeration.
+         */
+        public void visitCacheEntry (
+            PersistenceContext ctx, String cacheId, Serializable key, T record);
+    }
+
+    /**
+     * A simple implementation of {@link CacheTraverser} that selectively deletes entries in
+     * a cache depending on the return value of {@link #testCacheEntry}.
+     */
+    public static abstract class CacheEvictionFilter
+        implements CacheTraverser
+    {
+        // from CacheTraverser
+        public void visitCacheEntry (
+            PersistenceContext ctx, String cacheId, Serializable key, T record)
+        {
+            if (testForEviction(key, record)) {
+                ctx.cacheInvalidate(cacheId, key);
+            }
+        }
+
+        /**
+         * Decides whether or not this entry should be evicted and returns true if yes, false if
+         * no.
+         */
+        protected abstract boolean testForEviction (Serializable key, T record);
+    }
+    
+    /**
+     * Creates a persistence context that will use the supplied provider to obtain JDBC
+     * connections.
      *
-     * @param ident the identifier to provide to the connection provider when
-     * requesting a connection.
+     * @param ident the identifier to provide to the connection provider when requesting a
+     * connection.
      */
     public PersistenceContext (String ident, ConnectionProvider conprov)
     {
         _ident = ident;
         _conprov = conprov;
+        _cachemgr = CacheManager.getInstance();
     }
 
     /**
@@ -106,12 +164,28 @@ public class PersistenceContext
     /**
      * Invokes a non-modifying query and returns its result.
      */
-    @SuppressWarnings("unchecked")
     public  T invoke (Query query)
         throws PersistenceException
     {
-        // TODO: check the cache using query.getKey()
-        return (T) invoke(query, null, true);
+        CacheKey key = query.getCacheKey();
+        // if there is a cache key, check the cache
+        if (key != null) {
+            Cache cache = getCache(key.getCacheId());
+            if (cache != null) {
+                Element cacheHit = cache.get(key.getCacheKey());
+                if (cacheHit != null) {
+                    Log.debug("invoke: cache hit [hit=" + cacheHit + "]");
+                    @SuppressWarnings("unchecked") T value = (T) cacheHit.getValue();
+                    return value;
+                }
+            }
+            Log.debug("invoke: cache miss [key=" + key + "]");
+        }
+        // otherwise, perform the query
+        @SuppressWarnings("unchecked") T result = (T) invoke(query, null, true);
+        // and let the caller figure out if it wants to cache itself somehow
+        query.updateCache(this, result);
+        return result;
     }
 
     /**
@@ -120,11 +194,143 @@ public class PersistenceContext
     public int invoke (Modifier modifier)
         throws PersistenceException
     {
-        // TODO: invalidate the cache using the modifier's key
+        modifier.cacheInvalidation(this);
+        int rows = (Integer) invoke(null, modifier, true);
+        if (rows > 0) {
+            modifier.cacheUpdate(this);
+        }
+        return rows;
+    }
 
-        int result = (Integer) invoke(null, modifier, true);
-        // TODO: (optionally) cache the results of the modifier
-        return result;
+    /**
+     * Returns the {@link Cache} for the given cache id, or creates one if necessary.
+     */
+    public Cache getCache (String cacheId)
+    {
+        Cache cache = _cachemgr.getCache(cacheId);
+        if (cache == null) {
+            cache = new Cache(cacheId, 5000, false, false, 600, 60);
+            _cachemgr.addCache(cache);
+        }
+        return cache;
+    }
+
+    /**
+     * Stores a new entry indexed by the given key.
+     */
+    public  void cacheStore (CacheKey key, T value)
+    {
+        Log.debug("cacheStore: entry [key=" + key + ", value=" + value + "]");
+        getCache(key.getCacheId()).put(new Element(key.getCacheKey(), value));
+
+        // first do cascading invalidations
+        Set> listeners = _listenerSets.get(key.getCacheId());
+        if (listeners != null && listeners.size() > 0) {
+            for (CacheListener listener : listeners) {
+                Log.debug("cacheInvalidate: cascading [listener=" + listener + "]");
+                @SuppressWarnings("unchecked") CacheListener casted = (CacheListener)listener;
+                casted.entryModified(key, value);
+            }
+        }
+    }
+
+    /**
+     * Evicts the cache entry indexed under the given key, if there is one.
+     * The eviction may trigger further cache invalidations.
+     */
+    public void cacheInvalidate (CacheKey key)
+    {
+        cacheInvalidate(key.getCacheId(), key.getCacheKey());
+    }
+
+    /**
+     * Evicts the cache entry indexed under the given class and cache key, if there is one.
+     * The eviction may trigger further cache invalidations.
+     */
+    public void cacheInvalidate (Class pClass, Serializable cacheKey)
+    {
+        cacheInvalidate(pClass.getName(), cacheKey);
+    }
+
+    /**
+     * Evicts the cache entry indexed under the given cache id and cache key, if there is one.
+     * The eviction may trigger further cache invalidations.
+     */
+    public  void cacheInvalidate (String cacheId, Serializable cacheKey)
+    {
+        Log.debug("cacheInvalidate: entry [cacheId=" + cacheId + ", cacheKey=" + cacheKey + "]");
+        Cache cache = getCache(cacheId);
+        Element element = cache.get(cacheKey);
+
+        // first do cascading invalidations
+        Set> listeners = _listenerSets.get(cacheId);
+        if (listeners != null && listeners.size() > 0) {
+            CacheKey key = new SimpleCacheKey(cacheId, cacheKey);
+            for (CacheListener listener : listeners) {
+                Log.debug("cacheInvalidate: cascading [listener=" + listener + "]");
+                @SuppressWarnings("unchecked") CacheListener casted = (CacheListener)listener;
+                @SuppressWarnings("unchecked") T value =
+                    (element != null ? (T) element.getValue() : null);
+                casted.entryModified(key, value);
+            }
+        }
+
+        // then evict the keyed entry, if needed
+        if (element != null) {
+            Log.debug("cacheInvalidate: evicting [cacheKey=" + cacheKey + "]");
+            cache.remove(cacheKey);
+        }
+    }
+
+    /**
+     * Brutally iterates over the entire contents of the cache associated with the given class,
+     * invoking the callback for each cache entry.
+     */
+    public  void cacheTraverse (Class pClass, CacheTraverser filter)
+    {
+        cacheTraverse(pClass.getName(), filter);
+    }
+
+    /**
+     * Brutally iterates over the entire contents of the cache identified by the given cache id,
+     * invoking the callback for each cache entry.
+     */
+    public  void cacheTraverse (String cacheId, CacheTraverser filter)
+    {
+        Cache cache = getCache(cacheId);
+        if (cache != null) {
+            for (Object key : cache.getKeys()) {
+                Serializable sKey = (Serializable) key;
+                Element element = cache.get(sKey);
+                if (element != null) {
+                    @SuppressWarnings("unchecked") T value = (T) element.getValue();
+                    filter.visitCacheEntry(this, cacheId, sKey, value);
+                }
+            }
+        }
+    }
+
+    /**
+     * Registers a new cache listener with the cache associated with the given class.
+     */
+    public  void addCacheListener (
+        Class pClass, CacheListener listener)
+    {
+        addCacheListener(pClass.getName(), listener);
+    }
+
+    /**
+     * Registers a new cache listener with the identified cache.
+     */
+    public  void addCacheListener (
+        String cacheId, CacheListener listener)
+    {
+        Set> listenerSet = _listenerSets.get(cacheId);
+        if (listenerSet == null) {
+            listenerSet = new HashSet>();
+            _listenerSets.put(cacheId, listenerSet);
+        }
+        listenerSet.add(listener);
     }
 
     /**
@@ -203,22 +409,24 @@ public class PersistenceContext
     }
 
     /**
-     * Check whether the specified exception is a transient failure
-     * that can be retried.
+     * Check whether the specified exception is a transient failure that can be retried.
      */
     protected boolean isTransientException (SQLException sqe)
     {
         // TODO: this is MySQL specific. This was snarfed from MySQLLiaison.
         String msg = sqe.getMessage();
-        return (msg != null &&
-            (msg.indexOf("Lost connection") != -1 ||
-             msg.indexOf("link failure") != -1 ||
-             msg.indexOf("Broken pipe") != -1));
+        return (msg != null && (msg.indexOf("Lost connection") != -1 ||
+                                msg.indexOf("link failure") != -1 ||
+                                msg.indexOf("Broken pipe") != -1));
     }
 
     protected String _ident;
     protected ConnectionProvider _conprov;
+    protected CacheManager _cachemgr;
 
-    protected HashMap, DepotMarshaller> _marshallers =
+    protected Map>> _listenerSets =
+        new HashMap>>(); 
+
+    protected Map, DepotMarshaller> _marshallers =
         new HashMap, DepotMarshaller>();
 }
diff --git a/src/java/com/samskivert/jdbc/depot/Query.java b/src/java/com/samskivert/jdbc/depot/Query.java
index f0a95be..58b8cc5 100644
--- a/src/java/com/samskivert/jdbc/depot/Query.java
+++ b/src/java/com/samskivert/jdbc/depot/Query.java
@@ -21,277 +21,26 @@
 package com.samskivert.jdbc.depot;
 
 import java.sql.Connection;
-import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import com.samskivert.io.PersistenceException;
-import com.samskivert.jdbc.depot.annotation.Computed;
-import com.samskivert.jdbc.depot.clause.FieldOverride;
-import com.samskivert.jdbc.depot.clause.ForUpdate;
-import com.samskivert.jdbc.depot.clause.FromOverride;
-import com.samskivert.jdbc.depot.clause.GroupBy;
-import com.samskivert.jdbc.depot.clause.Join;
-import com.samskivert.jdbc.depot.clause.Limit;
-import com.samskivert.jdbc.depot.clause.OrderBy;
-import com.samskivert.jdbc.depot.clause.QueryClause;
-import com.samskivert.jdbc.depot.clause.Where;
-
 /**
- * Encapsulates a non-modifying query of persistent objects.
+ * The base of all read-only queries.
  */
-public abstract class Query
+public interface Query
 {
+    /**
+     * Returns the {@link CacheKey} associated with this query, if relevant, or null.
+     */
+    public CacheKey getCacheKey ();
+
     /**
      * Performs the actual JDBC operations associated with this query.
      */
-    public abstract T invoke (Connection conn) throws SQLException;
-
+    public T invoke (Connection conn)
+        throws SQLException;
+    
     /**
-     * Maps a class referenced by this query to its associated table.
-     *
-     * This method is called by individual clauses.
+     * Overriden by subclasses to perform case-by-case cache updates.
      */
-    public String getTableName (Class cl)
-    {
-        return _classMap.get(cl).getTableName();
-    }
-
-    /**
-     * Maps a class referenced by this query to the abbreviation used for its associated table. The
-     * abbreviation is transientand only has meaning within the scope of this particular query.
-     *
-     * This method is called by individual clauses.
-     */
-    public String getTableAbbreviation (Class cl)
-    {
-        int ix = _classList.indexOf(cl);
-        if (ix < 0) {
-            throw new IllegalArgumentException("Unknown persistence class: " + cl);
-        }
-        return "T" + (ix+1);
-    }
-
-    /**
-     * Translates the data in this Query object into a SQL query suited to instantiate a persistent
-     * object given the supplied key and clauses.  These clauses are assumed to contain no
-     * conflicts.
-     *
-     * If no key is supplied all instances will be loaded.
-     */
-    public  PreparedStatement createQuery (Connection conn)
-        throws SQLException
-    {
-        if (_mainType == null) { // internal error
-            throw new RuntimeException("createQuery() called with _mainClass == null");
-        }
-
-        DepotMarshaller mainMarshaller = _classMap.get(_mainType);
-        String[] fields = mainMarshaller._allFields;
-        StringBuilder query = new StringBuilder("select ");
-        boolean skip = true;
-        for (int ii = 0; ii < fields.length; ii ++) {
-            if (!skip) {
-                query.append(", ");
-            }
-            skip = false;
-
-            FieldOverride clause = _disMap.get(fields[ii]);
-            if (clause != null) {
-                clause.appendClause(this, query);
-                continue;
-            }
-
-            Computed computed = mainMarshaller._fields.get(fields[ii]).getComputed();
-            if (computed == null) {
-                // make sure the object corresponds to a table, otherwise the whole thing is
-                // computed
-                if (mainMarshaller.getTableName() != null) {
-                    // if it's neither overridden nor computed, it's a standard field
-                    query.append(getTableAbbreviation(_mainType)).append(".").append(fields[ii]);
-                    continue;
-                }
-                throw new SQLException(
-                    "@Computed entity field without definition [field=" + fields[ii] + "]");
-            }
-
-            // check if the computed field has a literal SQL definition
-            if (computed.fieldDefinition().length() > 0) {
-                query.append(computed.fieldDefinition() + " as " + fields[ii]);
-
-            } else if (!computed.required()) {
-                // or if we can simply ignore the field
-                skip = true;
-
-            } else {
-                throw new SQLException(
-                    "@Computed(required) field without definition [field=" + fields[ii] + "]");
-            }
-        }
-
-        if (_fromOverride != null) {
-            _fromOverride.appendClause(this, query);
-        } else if (mainMarshaller.getTableName() != null) {
-            query.append(" from ").append(mainMarshaller.getTableName());
-            query.append(" as ").append(getTableAbbreviation(_mainType));
-        } else {
-            throw new SQLException("Query on @Computed entity with no FromOverrideClause.");
-        }
-
-        for (Join clause : _joinClauses) {
-            clause.appendClause(this, query);
-        }
-        if (_where != null) {
-            _where.appendClause(this, query);
-        }
-        if (_groupBy != null) {
-            _groupBy.appendClause(this, query);
-        }
-        if (_orderBy != null) {
-            _orderBy.appendClause(this, query);
-        }
-        if (_limit != null) {
-            _limit.appendClause(this, query);
-        }
-        if (_forUpdate != null) {
-            _forUpdate.appendClause(this, query);
-        }
-
-        PreparedStatement pstmt = conn.prepareStatement(query.toString());
-        int argIdx = 1;
-        for (Join clause : _joinClauses) {
-            argIdx = clause.bindArguments(pstmt, argIdx);
-        }
-        if (_where != null) {
-            argIdx = _where.bindArguments(pstmt, argIdx);
-        }
-        if (_groupBy != null) {
-            argIdx = _groupBy.bindArguments(pstmt, argIdx);
-        }
-        if (_orderBy != null) {
-            argIdx = _orderBy.bindArguments(pstmt, argIdx);
-        }
-        if (_limit != null) {
-            argIdx = _limit.bindArguments(pstmt, argIdx);
-        }
-        if (_forUpdate != null) {
-            argIdx = _forUpdate.bindArguments(pstmt, argIdx);
-        }
-
-        return pstmt;
-    }
-
-    /**
-     * Creates a new Query object to generate one or more instances of the specified persistent
-     * class, as dictated by the key and query clauses.  A persistence context is supplied for
-     * instantiation of marshallers, which may trigger table creations and schema migrations.
-     */
-    protected Query (PersistenceContext ctx, Class type, QueryClause... clauses)
-        throws PersistenceException
-    {
-        _mainType = type;
-        Set classSet = new HashSet();
-        if (type != null) {
-            classSet.add(type);
-        }
-
-        for (QueryClause clause : clauses) {
-            if (clause instanceof Where) {
-                if (_where != null) {
-                    throw new IllegalArgumentException(
-                        "Query can't contain multiple Where clauses.");
-                }
-                _where = (Where) clause;
-
-            } else if (clause instanceof FromOverride) {
-                if (_fromOverride != null) {
-                    throw new IllegalArgumentException(
-                        "Query can't contain multiple FromOverride clauses.");
-                }
-                _fromOverride = (FromOverride) clause;
-                classSet.addAll(_fromOverride.getClassSet());
-
-            } else if (clause instanceof Join) {
-                _joinClauses.add((Join) clause);
-                classSet.addAll(((Join) clause).getClassSet());
-
-            } else if (clause instanceof FieldOverride) {
-                _disMap.put(((FieldOverride) clause).getField(),
-                            ((FieldOverride) clause));
-
-            } else if (clause instanceof OrderBy) {
-                if (_orderBy != null) {
-                    throw new IllegalArgumentException(
-                        "Query can't contain multiple OrderBy clauses.");
-                }
-                _orderBy = (OrderBy) clause;
-
-            } else if (clause instanceof GroupBy) {
-                if (_groupBy != null) {
-                    throw new IllegalArgumentException(
-                        "Query can't contain multiple GroupBy clauses.");
-                }
-                _groupBy = (GroupBy) clause;
-
-            } else if (clause instanceof Limit) {
-                if (_limit != null) {
-                    throw new IllegalArgumentException(
-                        "Query can't contain multiple Limit clauses.");
-                }
-                _limit = (Limit) clause;
-
-            } else if (clause instanceof ForUpdate) {
-                if (_forUpdate != null) {
-                    throw new IllegalArgumentException(
-                        "Query can't contain multiple For Update clauses.");
-                }
-                _forUpdate = (ForUpdate) clause;
-            }
-        }
-        _classMap = new HashMap();
-
-        for (Class c : classSet) {
-            _classMap.put(c, ctx.getMarshaller(c));
-        }
-        _classList = new ArrayList(classSet);
-    }
-
-    /** The persistent class to instantiate for the results. */
-    protected Class _mainType;
-
-    /** A list of referenced classes, used to generate table abbreviations. */
-    protected List _classList;
-
-    /** Classes mapped to marshallers, used for table names and field lists. */
-    protected Map _classMap;
-
-    /** Persistent class fields mapped to field override clauses. */
-    protected Map _disMap = new HashMap();
-
-    /** The from override clause, if any. */
-    protected FromOverride _fromOverride;
-
-    /** The where clause. */
-    protected Where _where;
-
-    /** A list of join clauses, each potentially referencing a new class. */
-    protected List _joinClauses = new ArrayList();
-
-    /** The order by clause, if any. */
-    protected OrderBy _orderBy;
-
-    /** The group by clause, if any. */
-    protected GroupBy _groupBy;
-
-    /** The limit clause, if any. */
-    protected Limit _limit;
-
-    /** The For Update clause, if any. */
-    protected ForUpdate _forUpdate;
+    void updateCache (PersistenceContext ctx, T result);
 }
diff --git a/src/java/com/samskivert/jdbc/depot/SimpleCacheKey.java b/src/java/com/samskivert/jdbc/depot/SimpleCacheKey.java
new file mode 100644
index 0000000..17545ee
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/SimpleCacheKey.java
@@ -0,0 +1,113 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot;
+
+import java.io.Serializable;
+
+/**
+ * Convenience class that implements {@link CacheKey} as simply as possibly. This class is
+ * typically used when the caller wants to cache a non-obvious query such as a collection,
+ * and needs to specify their own cache key and file it under a hand-picked cache id.
+ */
+public class SimpleCacheKey
+    implements CacheKey
+{
+    /**
+     * Construct a {@link SimpleCacheKey} for a query that has no parameters whatsoever.
+     */
+    public SimpleCacheKey (String cacheId)
+    {
+        this(cacheId, Boolean.TRUE);
+    }
+
+    /**
+     * Construct a {@link SimpleCacheKey} associated with the given persistent class with
+     * the given cache key.
+     */
+    public SimpleCacheKey (Class cacheClass, Serializable cacheKey)
+    {
+        this(cacheClass.getName(), cacheKey);
+    }
+
+    /**
+     * Construct a {@link SimpleCacheKey} for the given cache id with the given cache key.
+     */
+    public SimpleCacheKey (String cacheId, Serializable value)
+    {
+        _cacheId = cacheId;
+        _cacheKey = value;
+    }
+
+    // from CacheKey
+    public String getCacheId ()
+    {
+        return _cacheId;
+    }
+
+    // from CacheKey
+    public Serializable getCacheKey ()
+    {
+        return _cacheKey;
+    }
+
+    @Override
+    public int hashCode ()
+    {
+        final int PRIME = 31;
+        int result = 1;
+        result = PRIME * result + ((_cacheId == null) ? 0 : _cacheId.hashCode());
+        result = PRIME * result + ((_cacheKey == null) ? 0 : _cacheKey.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals (Object obj)
+    {
+        if (obj == null || obj.getClass() != getClass()) {
+            return false;
+        }
+        SimpleCacheKey other = (SimpleCacheKey) obj;
+        if (_cacheId == null) {
+            if (other._cacheId != null) {
+                return false;
+            }
+        } else if (!_cacheId.equals(other._cacheId)) {
+            return false;
+        }
+        if (_cacheKey == null) {
+            if (other._cacheKey != null) {
+                return false;
+            }
+        } else if (!_cacheKey.equals(other._cacheKey)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString ()
+    {
+        return "[cacheId=" + _cacheId + ", value=" + _cacheKey + "]";
+    }
+
+    protected String _cacheId;
+    protected Serializable _cacheKey;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/annotation/Computed.java b/src/java/com/samskivert/jdbc/depot/annotation/Computed.java
index 7580a5d..6cf3e8b 100644
--- a/src/java/com/samskivert/jdbc/depot/annotation/Computed.java
+++ b/src/java/com/samskivert/jdbc/depot/annotation/Computed.java
@@ -25,11 +25,11 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * Marks a field as computed, meaning it is ignored for schema purposes and it does not directly
- * correspond to a column in a table, thus its value must be overridden in the {@link Query}.
+ * correspond to a column in a table, thus its value must be overridden in the {@link ConstructedQuery}.
  */
 @Retention(value=RetentionPolicy.RUNTIME)
 @Target({ ElementType.FIELD, ElementType.TYPE })
diff --git a/src/java/com/samskivert/jdbc/depot/clause/FieldOverride.java b/src/java/com/samskivert/jdbc/depot/clause/FieldOverride.java
index c2f5e2b..ef04dcf 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/FieldOverride.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/FieldOverride.java
@@ -25,7 +25,7 @@ import java.sql.SQLException;
 import java.util.Collection;
 
 import com.samskivert.io.PersistenceException;
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.expression.ColumnExp;
 import com.samskivert.jdbc.depot.expression.LiteralExp;
 import com.samskivert.jdbc.depot.expression.SQLExpression;
@@ -75,7 +75,7 @@ public class FieldOverride
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         _override.appendExpression(query, builder);
         builder.append(" as ").append(_field);
diff --git a/src/java/com/samskivert/jdbc/depot/clause/ForUpdate.java b/src/java/com/samskivert/jdbc/depot/clause/ForUpdate.java
index a2fe165..20972ed 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/ForUpdate.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/ForUpdate.java
@@ -24,7 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  *  Represents a FOR UPDATE clause.
@@ -39,7 +39,7 @@ public class ForUpdate
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" for update ");
     }
diff --git a/src/java/com/samskivert/jdbc/depot/clause/FromOverride.java b/src/java/com/samskivert/jdbc/depot/clause/FromOverride.java
index e37082c..f9a1366 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/FromOverride.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/FromOverride.java
@@ -26,7 +26,7 @@ import java.util.Arrays;
 import java.util.Collection;
 
 import com.samskivert.io.PersistenceException;
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  *  Completely overrides the FROM clause, if it exists.
@@ -47,7 +47,7 @@ public class FromOverride
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" from " );
         for (int ii = 0; ii < _fromClasses.length; ii++) {
diff --git a/src/java/com/samskivert/jdbc/depot/clause/GroupBy.java b/src/java/com/samskivert/jdbc/depot/clause/GroupBy.java
index 5eb0437..a40ca05 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/GroupBy.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/GroupBy.java
@@ -24,7 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.expression.SQLExpression;
 
 /**
@@ -45,7 +45,7 @@ public class GroupBy
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" group by ");
         for (int ii = 0; ii < _values.length; ii++) {
diff --git a/src/java/com/samskivert/jdbc/depot/clause/Join.java b/src/java/com/samskivert/jdbc/depot/clause/Join.java
index 1183e3e..52a10b3 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/Join.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/Join.java
@@ -26,7 +26,7 @@ import java.util.Arrays;
 import java.util.Collection;
 
 import com.samskivert.io.PersistenceException;
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.expression.ColumnExp;
 import com.samskivert.jdbc.depot.operator.SQLOperator;
 import com.samskivert.jdbc.depot.operator.Conditionals.*;
@@ -64,7 +64,7 @@ public class Join
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" inner join " );
         builder.append(query.getTableName(_joinClass)).append(" as ");
diff --git a/src/java/com/samskivert/jdbc/depot/clause/Limit.java b/src/java/com/samskivert/jdbc/depot/clause/Limit.java
index b54c03f..50063d5 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/Limit.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/Limit.java
@@ -24,7 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  *  Represents a LIMIT/OFFSET clause, for pagination.
@@ -45,7 +45,7 @@ public class Limit
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" limit ? offset ? ");
     }
diff --git a/src/java/com/samskivert/jdbc/depot/clause/OrderBy.java b/src/java/com/samskivert/jdbc/depot/clause/OrderBy.java
index 7ddc391..dc24891 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/OrderBy.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/OrderBy.java
@@ -24,7 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.expression.ColumnExp;
 import com.samskivert.jdbc.depot.expression.SQLExpression;
 
@@ -82,7 +82,7 @@ public class OrderBy
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" order by ");
         for (int ii = 0; ii < _values.length; ii++) {
diff --git a/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java b/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java
index b944037..4c0a8ad 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java
@@ -24,7 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * Represents a piece or modifier of an SQL query.
@@ -41,7 +41,7 @@ public interface QueryClause
      * Construct the SQL form of this query clause. The implementor is expected to call methods
      * on the Query object to e.g. resolve current table abbreviations associated with classes.
      */
-    public void appendClause (Query query, StringBuilder builder);
+    public void appendClause (ConstructedQuery query, StringBuilder builder);
 
     /**
      * Bind any objects that were referenced in the generated SQL.  For each ? that appears in the
diff --git a/src/java/com/samskivert/jdbc/depot/clause/Where.java b/src/java/com/samskivert/jdbc/depot/clause/Where.java
index 93c120b..3fcb0ba 100644
--- a/src/java/com/samskivert/jdbc/depot/clause/Where.java
+++ b/src/java/com/samskivert/jdbc/depot/clause/Where.java
@@ -25,9 +25,14 @@ import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.List;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.clause.QueryClause;
+import com.samskivert.jdbc.depot.expression.ColumnExp;
+import com.samskivert.jdbc.depot.expression.ValueExp;
 import com.samskivert.jdbc.depot.operator.SQLOperator;
+import com.samskivert.jdbc.depot.operator.Conditionals.Equals;
+import com.samskivert.jdbc.depot.operator.Conditionals.IsNull;
+import com.samskivert.jdbc.depot.operator.Logic.And;
 
 /**
  * Represents a where clause: the condition can be any comparison operator or logical combination
@@ -36,6 +41,47 @@ import com.samskivert.jdbc.depot.operator.SQLOperator;
 public class Where
     implements QueryClause
 {
+    public Where (String index, Comparable value)
+    {
+        this(new ColumnExp(index), value);
+    }
+
+    public Where (ColumnExp column, Comparable value)
+    {
+        this(new ColumnExp[] { column }, new Comparable[] { value });
+    }
+
+    public Where (ColumnExp index1, Comparable value1,
+                ColumnExp index2, Comparable value2)
+    {
+        this(new ColumnExp[] { index1, index2 }, new Comparable[] { value1, value2 });
+    }
+
+    public Where (ColumnExp index1, Comparable value1,
+                ColumnExp index2, Comparable value2,
+                ColumnExp index3, Comparable value3)
+    {
+        this(new ColumnExp[] { index1, index2, index3 },
+             new Comparable[] { value1, value2, value3 });
+    }
+
+    public Where (String index1, Comparable value1, String index2, Comparable value2)
+    {
+        this(new ColumnExp(index1), value1, new ColumnExp(index2), value2);
+    }
+
+    public Where (String index1, Comparable value1, String index2, Comparable value2,
+                String index3, Comparable value3)
+    {
+        this(new ColumnExp(index1), value1, new ColumnExp(index2), value2, 
+             new ColumnExp(index3), value3);
+    }
+
+    public Where (ColumnExp[] columns, Comparable[] values)
+    {
+        this(toCondition(columns, values));
+    }
+
     public Where (SQLOperator condition)
     {
         _condition = condition;
@@ -48,7 +94,7 @@ public class Where
     }
 
     // from QueryClause
-    public void appendClause (Query query, StringBuilder builder)
+    public void appendClause (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(" where ");
         _condition.appendExpression(query, builder);
@@ -61,5 +107,15 @@ public class Where
         return _condition.bindArguments(pstmt, argIdx);
     }
 
+    protected static SQLOperator toCondition (ColumnExp[] columns, Comparable[] values)
+    {
+        SQLOperator[] comparisons = new SQLOperator[columns.length];
+        for (int ii = 0; ii < columns.length; ii ++) {
+            comparisons[ii] = (values[ii] == null) ? new IsNull(columns[ii]) :
+                new Equals(columns[ii], new ValueExp(values[ii]));
+        }
+        return new And(comparisons);
+    }
+
     protected SQLOperator _condition;
 }
diff --git a/src/java/com/samskivert/jdbc/depot/expression/ColumnExp.java b/src/java/com/samskivert/jdbc/depot/expression/ColumnExp.java
index 19022fa..6f811c7 100644
--- a/src/java/com/samskivert/jdbc/depot/expression/ColumnExp.java
+++ b/src/java/com/samskivert/jdbc/depot/expression/ColumnExp.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.expression;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * An expression identifying a column of a class, e.g. GameRecord.itemId. If no class is given,
@@ -51,7 +51,7 @@ public class ColumnExp
     }
 
     // from SQLExpression
-    public void appendExpression (Query query, StringBuilder builder)
+    public void appendExpression (ConstructedQuery query, StringBuilder builder)
     {
         if (pClass == null || query == null) {
             builder.append(pColumn);
diff --git a/src/java/com/samskivert/jdbc/depot/expression/FunctionExp.java b/src/java/com/samskivert/jdbc/depot/expression/FunctionExp.java
index acdcd21..6c9abee 100644
--- a/src/java/com/samskivert/jdbc/depot/expression/FunctionExp.java
+++ b/src/java/com/samskivert/jdbc/depot/expression/FunctionExp.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.expression;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * An expression for a function, e.g. FLOOR(blah).
@@ -41,7 +41,7 @@ public class FunctionExp
     }
 
     // from SQLExpression
-    public void appendExpression (Query query, StringBuilder builder)
+    public void appendExpression (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(_function);
         builder.append("(");
diff --git a/src/java/com/samskivert/jdbc/depot/expression/LiteralExp.java b/src/java/com/samskivert/jdbc/depot/expression/LiteralExp.java
index 3befae5..b8961b7 100644
--- a/src/java/com/samskivert/jdbc/depot/expression/LiteralExp.java
+++ b/src/java/com/samskivert/jdbc/depot/expression/LiteralExp.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.expression;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * An expression for things we don't support natively, e.g. COUNT(*).
@@ -38,7 +38,7 @@ public class LiteralExp
     }
 
     // from SQLExpression
-    public void appendExpression (Query query, StringBuilder builder)
+    public void appendExpression (ConstructedQuery query, StringBuilder builder)
     {
         builder.append(_text);
     }
diff --git a/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java b/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java
index 69d32ca..dc2a0ad 100644
--- a/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java
+++ b/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.expression;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * Represents an SQL expression, e.g. column name, function, or constant.
@@ -34,7 +34,7 @@ public interface SQLExpression
      * Construct the SQL form of this expression. The implementor is invited to call methods on the
      * Query object to e.g. resolve the current table abbreviations associated with classes.
      */
-    public void appendExpression (Query query, StringBuilder builder);
+    public void appendExpression (ConstructedQuery query, StringBuilder builder);
 
     /**
      * Bind any objects that were referenced in the generated SQL.  For each ? that appears in the
diff --git a/src/java/com/samskivert/jdbc/depot/expression/ValueExp.java b/src/java/com/samskivert/jdbc/depot/expression/ValueExp.java
index 8f1e4be..aafd4b1 100644
--- a/src/java/com/samskivert/jdbc/depot/expression/ValueExp.java
+++ b/src/java/com/samskivert/jdbc/depot/expression/ValueExp.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.expression;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 
 /**
  * A Java value that is bound as a parameter to the query, e.g. 1 or 'abc'. 
@@ -37,7 +37,7 @@ public class ValueExp
     }
 
     // from SQLExpression
-    public void appendExpression (Query query, StringBuilder builder)
+    public void appendExpression (ConstructedQuery query, StringBuilder builder)
     {
         builder.append("?");
     }
diff --git a/src/java/com/samskivert/jdbc/depot/operator/Conditionals.java b/src/java/com/samskivert/jdbc/depot/operator/Conditionals.java
index fe4c936..9270014 100644
--- a/src/java/com/samskivert/jdbc/depot/operator/Conditionals.java
+++ b/src/java/com/samskivert/jdbc/depot/operator/Conditionals.java
@@ -24,7 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.expression.ColumnExp;
 import com.samskivert.jdbc.depot.expression.SQLExpression;
 import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator;
@@ -56,7 +56,7 @@ public abstract class Conditionals
         }
 
         // from SQLExpression
-        public void appendExpression (Query query, StringBuilder builder)
+        public void appendExpression (ConstructedQuery query, StringBuilder builder)
         {
             _column.appendExpression(query, builder);
             builder.append(" is null");
@@ -181,7 +181,7 @@ public abstract class Conditionals
         }
 
         // from SQLExpression
-        public void appendExpression (Query query, StringBuilder builder)
+        public void appendExpression (ConstructedQuery query, StringBuilder builder)
         {
             _column.appendExpression(query, builder);
             builder.append(" in (");
@@ -234,7 +234,7 @@ public abstract class Conditionals
         }
 
         // from SQLExpression
-        public void appendExpression (Query query, StringBuilder builder)
+        public void appendExpression (ConstructedQuery query, StringBuilder builder)
         {
             builder.append("match(");
             int idx = 0;
diff --git a/src/java/com/samskivert/jdbc/depot/operator/Logic.java b/src/java/com/samskivert/jdbc/depot/operator/Logic.java
index f84c7c1..bd8c83f 100644
--- a/src/java/com/samskivert/jdbc/depot/operator/Logic.java
+++ b/src/java/com/samskivert/jdbc/depot/operator/Logic.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.operator;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator;
 
 /**
@@ -80,7 +80,7 @@ public abstract class Logic
         }
 
         // from SQLExpression
-        public void appendExpression (Query query, StringBuilder builder)
+        public void appendExpression (ConstructedQuery query, StringBuilder builder)
         {
             builder.append(" not (");
             _condition.appendExpression(query, builder);
diff --git a/src/java/com/samskivert/jdbc/depot/operator/SQLOperator.java b/src/java/com/samskivert/jdbc/depot/operator/SQLOperator.java
index 49672dc..e5d897d 100644
--- a/src/java/com/samskivert/jdbc/depot/operator/SQLOperator.java
+++ b/src/java/com/samskivert/jdbc/depot/operator/SQLOperator.java
@@ -23,7 +23,7 @@ package com.samskivert.jdbc.depot.operator;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
-import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.ConstructedQuery;
 import com.samskivert.jdbc.depot.expression.SQLExpression;
 import com.samskivert.jdbc.depot.expression.ValueExp;
 
@@ -47,7 +47,7 @@ public interface SQLOperator extends SQLExpression
         }
 
         // from SQLExpression
-        public void appendExpression (Query query, StringBuilder builder)
+        public void appendExpression (ConstructedQuery query, StringBuilder builder)
         {
             for (int ii = 0; ii < _conditions.length; ii++) {
                 if (ii > 0) {
@@ -94,7 +94,7 @@ public interface SQLOperator extends SQLExpression
         }
 
         // from SQLExpression
-        public void appendExpression (Query query, StringBuilder builder)
+        public void appendExpression (ConstructedQuery query, StringBuilder builder)
         {
             _lhs.appendExpression(query, builder);
             builder.append(operator());