From 152ff056d97774c8702dc7aafbdc017ec97015b1 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 23 Feb 2010 07:30:58 +0000 Subject: [PATCH] Charlie has convinced me that the database manipulation operations should be public and I should not force everyone who wants to use Depot to route all database accesses through custom public methods that hide all of the underlying Depot business behind application specific APIs. I still think this is the best way to use Depot, but I acknowledge that there are certain valid use cases (like generic code that operates on records in arbitrary repositories) which are precluded by this scheme. So instead I'll just strongly suggest that Depot be used in the way I think it should, and frown sternly at anyone who deviates from The Way. --- .../com/samskivert/depot/DepotRepository.java | 1200 ++++++++--------- 1 file changed, 600 insertions(+), 600 deletions(-) diff --git a/src/java/com/samskivert/depot/DepotRepository.java b/src/java/com/samskivert/depot/DepotRepository.java index 2582a15..cd10210 100644 --- a/src/java/com/samskivert/depot/DepotRepository.java +++ b/src/java/com/samskivert/depot/DepotRepository.java @@ -125,6 +125,606 @@ public abstract class DepotRepository CONTENTS } + /** + * Loads the persistent object that matches the specified primary key. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public T load (Key key, QueryClause... clauses) + throws DatabaseException + { + return load(key, CacheStrategy.BEST, clauses); + } + + /** + * Loads the persistent object that matches the specified primary key. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public T load ( + Key key, CacheStrategy strategy, QueryClause... clauses) + throws DatabaseException + { + clauses = ArrayUtil.append(clauses, key); + return _ctx.invoke(new FindOneQuery(_ctx, key.getPersistentClass(), strategy, clauses)); + } + + /** + * Loads the first persistent object that matches the supplied query clauses. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public T load (Class type, QueryClause... clauses) + throws DatabaseException + { + return load(type, CacheStrategy.BEST, clauses); + } + + /** + * Loads the first persistent object that matches the supplied query clauses. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public T load ( + Class type, CacheStrategy strategy, QueryClause... clauses) + throws DatabaseException + { + return _ctx.invoke(new FindOneQuery(_ctx, type, strategy, clauses)); + } + + /** + * Loads up all persistent records that match the supplied set of raw primary keys. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList loadAll ( + Class type, Collection> primaryKeys) + throws DatabaseException + { + // convert the raw keys into real key records + DepotMarshaller marsh = _ctx.getMarshaller(type); + List> keys = Lists.newArrayList(); + for (Comparable key : primaryKeys) { + keys.add(marsh.makePrimaryKey(key)); + } + return loadAll(keys); + } + + /** + * Loads up all persistent records that match the supplied set of primary keys. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList loadAll (Collection> keys) + throws DatabaseException + { + return (keys.size() == 0) ? new XArrayList() : + _ctx.invoke(new FindAllQuery.WithKeys(_ctx, keys)); + } + + /** + * A varargs version of {@link #findAll(Class,Collection)}. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList findAll (Class type, QueryClause... clauses) + throws DatabaseException + { + return findAll(type, Arrays.asList(clauses)); + } + + /** + * Loads all persistent objects that match the specified clauses. + * + * We have two strategies for doing this: one performs the query as-is, the second executes two + * passes: first fetching only key columns and consulting the cache for each such key; then, in + * the second pass, fetching the full entity only for keys that were not found in the cache. + * + * The more complex strategy could save a lot of data shuffling. On the other hand, its + * complexity is an inherent drawback, and it does execute two separate database queries for + * what the simple method does in one. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList findAll ( + Class type, Collection clauses) + throws DatabaseException + { + return findAll(type, CacheStrategy.BEST, clauses); + } + + /** + * A varargs version of {@link #findAll(Class,CacheStrategy,Collection)}. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList findAll ( + Class type, CacheStrategy strategy, QueryClause... clauses) + throws DatabaseException + { + return findAll(type, strategy, Arrays.asList(clauses)); + } + + /** + * Loads all persistent objects that match the specified clauses. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList findAll ( + Class type, CacheStrategy cache, Collection clauses) + throws DatabaseException + { + DepotMarshaller marsh = _ctx.getMarshaller(type); + + switch (cache) { + case LONG_KEYS: case SHORT_KEYS: case BEST: case RECORDS: + String reason = null; + if (marsh.getTableName() == null) { + reason = type + " is computed"; + + } else if (!marsh.hasPrimaryKey()) { + reason = type + " has no primary key"; + + } else { + for (QueryClause clause : clauses) { + if (clause instanceof FieldOverride) { + reason = "query uses a FieldOverride clause"; + break; + } + } + } + if (cache == CacheStrategy.BEST) { + cache = (reason != null) ? CacheStrategy.NONE : CacheStrategy.SHORT_KEYS; + + } else if (reason != null) { + // if user explicitly asked for a strategy we can't do, protest + throw new IllegalArgumentException( + "Cannot use " + cache + " strategy because " + reason); + } + break; + + case NONE: case CONTENTS: + break; // NONE and CONTENTS can always be used. + } + + if (!_ctx.isUsingCache()) { + cache = CacheStrategy.NONE; + } + + switch (cache) { + case SHORT_KEYS: case LONG_KEYS: case RECORDS: + return _ctx.invoke(new FindAllQuery.WithCache(_ctx, type, clauses, cache)); + + default: + return _ctx.invoke(new FindAllQuery.Explicitly( + _ctx, type, clauses, cache == CacheStrategy.CONTENTS)); + } + } + + /** + * Looks up and returns {@link Key} records for all rows that match the supplied query clauses. + * + * @param forUpdate if true, the query will be run using a read-write connection to ensure that + * it talks to the master database, if false, the query will be run on a read-only connection + * and may load keys from a slave. For performance reasons, you should always pass false unless + * you know you will be modifying the database as a result of this query and absolutely need + * the latest data. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList> findAllKeys ( + Class type, boolean forUpdate, QueryClause... clause) + throws DatabaseException + { + return findAllKeys(type, forUpdate, Arrays.asList(clause)); + } + + /** + * Looks up and returns {@link Key} records for all rows that match the supplied query clauses. + * + * @param forUpdate if true, the query will be run using a read-write connection to ensure that + * it talks to the master database, if false, the query will be run on a read-only connection + * and may load keys from a slave. For performance reasons, you should always pass false unless + * you know you will be modifying the database as a result of this query and absolutely need + * the latest data. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public XList> findAllKeys ( + Class type, boolean forUpdate, Collection clauses) + throws DatabaseException + { + return _ctx.invoke(new FindAllKeysQuery(_ctx, type, forUpdate, clauses)); + } + + /** + * Inserts the supplied persistent object into the database, assigning its primary key (if it + * has one) in the process. + * + * @return the number of rows modified by this action, this should always be one. + * + * @throws DuplicateKeyException if the inserted record conflicts with the primary key (or any + * other unique key) of a record already in the database. + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int insert (T record) + throws DatabaseException + { + @SuppressWarnings("unchecked") final Class pClass = (Class) record.getClass(); + final DepotMarshaller marsh = _ctx.getMarshaller(pClass); + Key key = marsh.getPrimaryKey(record, false); + + DepotTypes types = DepotTypes.getDepotTypes(_ctx); + types.addClass(_ctx, pClass); + final SQLBuilder builder = _ctx.getSQLBuilder(types); + + // key will be null if record was supplied without a primary key + return _ctx.invoke(new CachingModifier(record, key, key) { + @Override + protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + // if needed, update our modifier's key so that it can cache our results + Set identityFields = Collections.emptySet(); + if (_key == null) { + // set any auto-generated column values + identityFields = marsh.generateFieldValues(conn, liaison, _result, false); + updateKey(marsh.getPrimaryKey(_result, false)); + } + + builder.newQuery(new InsertClause(pClass, _result, identityFields)); + + int mods = builder.prepare(conn).executeUpdate(); + // run any post-factum value generators and potentially generate our key + if (_key == null) { + marsh.generateFieldValues(conn, liaison, _result, true); + updateKey(marsh.getPrimaryKey(_result, false)); + } + return mods; + } + }); + } + + /** + * Updates all fields of the supplied persistent object, using its primary key to identify the + * row to be updated. + * + * @return the number of rows modified by this action. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int update (PersistentRecord record) + throws DatabaseException + { + Class pClass = record.getClass(); + requireNotComputed(pClass, "update"); + DepotMarshaller marsh = _ctx.getMarshaller(pClass); + Key key = marsh.getPrimaryKey(record); + if (key == null) { + throw new IllegalArgumentException("Can't update record with null primary key."); + } + return doUpdate(key, new UpdateClause(pClass, key, marsh.getColumnFieldNames(), record)); + } + + /** + * Updates just the specified fields of the supplied persistent object, using its primary key + * to identify the row to be updated. This method currently flushes the associated record from + * the cache, but in the future it should be modified to update the modified fields in the + * cached value iff the record exists in the cache. + * + * @return the number of rows modified by this action. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int update (T record, final ColumnExp... modifiedFields) + throws DatabaseException + { + @SuppressWarnings("unchecked") Class pClass = (Class) record.getClass(); + requireNotComputed(pClass, "update"); + DepotMarshaller marsh = _ctx.getMarshaller(pClass); + Key key = marsh.getPrimaryKey(record); + if (key == null) { + throw new IllegalArgumentException("Can't update record with null primary key."); + } + return doUpdate(key, new UpdateClause(pClass, key, modifiedFields, record)); + } + + /** + * Updates the specified columns for all persistent objects matching the supplied key. + * + * @param key the key for the persistent objects to be modified. + * @param field the first field to be updated. + * @param value the value to assign to the first field. This may be a primitive (Integer, + * String, etc.) which will be wrapped in value expression or a SQLExpression instance. + * @param more additional (field, value) pairs to be updated. + * + * @return the number of rows modified by this action. + * + * @throws DuplicateKeyException if the update attempts to change the key columns of a row to + * values that duplicate another row already in the database. + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int updatePartial ( + Key key, ColumnExp field, Object value, Object... more) + throws DatabaseException + { + return updatePartial(key.getPersistentClass(), key, key, field, value, more); + } + + /** + * Updates the specified columns for all persistent objects matching the supplied key. + * + * @param key the key for the persistent objects to be modified. + * @param updates a mapping from field to value for all values to be changed. The values may be + * primitives (Integer, String, etc.) which will be wrapped in value expression instances or + * SQLExpression instances defining the value. + * + * @return the number of rows modified by this action. + * + * @throws DuplicateKeyException if the update attempts to change the key columns of a row to + * values that duplicate another row already in the database. + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int updatePartial (Key key, Map updates) + throws DatabaseException + { + return updatePartial(key.getPersistentClass(), key, key, updates); + } + + /** + * Updates the specified columns for all persistent objects matching the supplied key. This + * method currently flushes the associated record from the cache, but in the future it should + * be modified to update the modified fields in the cached value iff the record exists in the + * cache. + * + * @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 run prior to the update to flush the + * relevant persistent objects from the cache, or null if no invalidation is needed. + * @param updates a mapping from field to value for all values to be changed. The values may be + * primitives (Integer, String, etc.) which will be wrapped in value expression instances or + * SQLExpression instances defining the value. + * + * @return the number of rows modified by this action. + * + * @throws DuplicateKeyException if the update attempts to change the key columns of a row to + * values that duplicate another row already in the database. + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int updatePartial ( + Class type, final WhereClause key, CacheInvalidator invalidator, + Map updates) + throws DatabaseException + { + // separate the arguments into keys and values + final ColumnExp[] fields = new ColumnExp[updates.size()]; + final SQLExpression[] values = new SQLExpression[fields.length]; + int ii = 0; + for (Map.Entry entry : updates.entrySet()) { + fields[ii] = entry.getKey(); + values[ii++] = makeValue(entry.getValue()); + } + return updatePartial(type, key, invalidator, fields, values); + } + + /** + * Updates the specified columns for all persistent objects matching the supplied key. This + * method currently flushes the associated record from the cache, but in the future it should + * be modified to update the modified fields in the cached value iff the record exists in the + * cache. + * + * @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 run prior to the update to flush the + * relevant persistent objects from the cache, or null if no invalidation is needed. + * @param field the first field to be updated. + * @param value the value to assign to the first field. This may be a primitive (Integer, + * String, etc.) which will be wrapped in value expression or a SQLExpression instance. + * @param more additional (field, value) pairs to be updated. + * + * @return the number of rows modified by this action. + * + * @throws DuplicateKeyException if the update attempts to change the key columns of a row to + * values that duplicate another row already in the database. + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int updatePartial ( + Class type, final WhereClause key, CacheInvalidator invalidator, + ColumnExp field, Object value, Object... more) + throws DatabaseException + { + // separate the updates into keys and values + final ColumnExp[] fields = new ColumnExp[1+more.length/2]; + final SQLExpression[] values = new SQLExpression[fields.length]; + fields[0] = field; + values[0] = makeValue(value); + for (int ii = 1, idx = 0; ii < fields.length; ii++) { + fields[ii] = (ColumnExp)more[idx++]; + values[ii] = makeValue(more[idx++]); + } + return updatePartial(type, key, invalidator, fields, values); + } + + /** + * Updates the specified columns for all persistent objects matching the supplied key. This + * method currently flushes the associated record from the cache, but in the future it should + * be modified to update the modified fields in the cached value iff the record exists in the + * cache. + * + * @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 run prior to the update to flush the + * relevant persistent objects from the cache, or null if no invalidation is needed. + * @param fields the fields in the objects to be updated. + * @param values the values to be assigned to the fields. + * + * @return the number of rows modified by this action. + * + * @throws DuplicateKeyException if the update attempts to change the key columns of a row to + * values that duplicate another row already in the database. + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int updatePartial ( + Class type, final WhereClause key, CacheInvalidator invalidator, + ColumnExp[] fields, SQLExpression[] values) + throws DatabaseException + { + requireNotComputed(type, "updatePartial"); + if (invalidator instanceof ValidatingCacheInvalidator) { + ((ValidatingCacheInvalidator)invalidator).validateFlushType(type); // sanity check + } + key.validateQueryType(type); // and another + return doUpdate(invalidator, new UpdateClause(type, key, fields, values)); + } + + /** + * Stores the supplied persisent object in the database. If it has no primary key assigned (it + * is null or zero), it will be inserted directly. Otherwise an update will first be attempted + * and if that matches zero rows, the object will be inserted. + * + * @return true if the record was created, false if it was updated. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public boolean store (T record) + throws DatabaseException + { + @SuppressWarnings("unchecked") final Class pClass = (Class) record.getClass(); + requireNotComputed(pClass, "store"); + + final DepotMarshaller marsh = _ctx.getMarshaller(pClass); + Key key = marsh.hasPrimaryKey() ? marsh.getPrimaryKey(record) : null; + final UpdateClause update = + new UpdateClause(pClass, key, marsh.getColumnFieldNames(), record); + final SQLBuilder builder = _ctx.getSQLBuilder(DepotTypes.getDepotTypes(_ctx, update)); + + // if our primary key isn't null, we start by trying to update rather than insert + if (key != null) { + builder.newQuery(update); + } + + final boolean[] created = new boolean[1]; + _ctx.invoke(new CachingModifier(record, key, key) { + @Override + protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + if (_key != null) { + // run the update + int mods = builder.prepare(conn).executeUpdate(); + if (mods > 0) { + // if it succeeded, we're done + return mods; + } + } + + // if the update modified zero rows or the primary key was unset, insert + Set identityFields = Collections.emptySet(); + if (_key == null) { + + // first, set any auto-generated column values + identityFields = marsh.generateFieldValues(conn, liaison, _result, false); + // update our modifier's key so that it can cache our results + updateKey(marsh.getPrimaryKey(_result, false)); + } + + builder.newQuery(new InsertClause(pClass, _result, identityFields)); + + int mods = builder.prepare(conn).executeUpdate(); + + // run any post-factum value generators and potentially generate our key + if (_key == null) { + marsh.generateFieldValues(conn, liaison, _result, true); + updateKey(marsh.getPrimaryKey(_result, false)); + } + created[0] = true; + return mods; + } + }); + return created[0]; + } + + /** + * Deletes all persistent objects from the database matching the primary key of the supplied + * object (which should be one or zero). + * + * @return the number of rows deleted by this action. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int delete (T record) + throws DatabaseException + { + @SuppressWarnings("unchecked") Class type = (Class)record.getClass(); + Key primaryKey = _ctx.getMarshaller(type).getPrimaryKey(record); + if (primaryKey == null) { + throw new IllegalArgumentException("Can't delete record with null primary key."); + } + return delete(primaryKey); + } + + /** + * Deletes all persistent objects from the database matching the supplied primary key (which + * should be one or zero). + * + * @return the number of rows deleted by this action. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int delete (Key primaryKey) + throws DatabaseException + { + return deleteAll(primaryKey.getPersistentClass(), primaryKey, primaryKey); + } + + /** + * Deletes all persistent objects from the database that match the supplied where clause. + * + * @return the number of rows deleted by this action. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int deleteAll (Class type, final WhereClause where) + throws DatabaseException + { + if (where instanceof CacheInvalidator) { + // our where clause knows how to do its own deletion, yay! + return deleteAll(type, where, (CacheInvalidator)where); + } else if (_ctx.getMarshaller(type).hasPrimaryKey()) { + // look up the primary keys for all matching rows matching and delete using those + KeySet pwhere = KeySet.newKeySet(type, findAllKeys(type, true, where)); + return deleteAll(type, pwhere, pwhere); + } else { + // otherwise just do the delete directly as we can't have cached a record that has no + // primary key in the first place + return deleteAll(type, where, null); + } + } + + /** + * Deletes all persistent objects from the database that match the supplied key. + * + * @return the number of rows deleted by this action. + * + * @throws DatabaseException if any problem is encountered communicating with the database. + */ + public int deleteAll ( + Class type, final WhereClause where, CacheInvalidator invalidator) + throws DatabaseException + { + if (invalidator instanceof ValidatingCacheInvalidator) { + ((ValidatingCacheInvalidator)invalidator).validateFlushType(type); // sanity check + } + where.validateQueryType(type); // and another + + DeleteClause delete = new DeleteClause(type, where); + final SQLBuilder builder = _ctx.getSQLBuilder(DepotTypes.getDepotTypes(_ctx, delete)); + builder.newQuery(delete); + + return _ctx.invoke(new Modifier(invalidator) { + @Override + protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + return builder.prepare(conn).executeUpdate(); + } + }); + } + /** * Creates a repository with the supplied persistence context. Any schema migrations needed by * this repository should be registered in its constructor. A repository should not @@ -209,606 +809,6 @@ public abstract class DepotRepository */ protected abstract void getManagedRecords (Set> classes); - /** - * Loads the persistent object that matches the specified primary key. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected T load (Key key, QueryClause... clauses) - throws DatabaseException - { - return load(key, CacheStrategy.BEST, clauses); - } - - /** - * Loads the persistent object that matches the specified primary key. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected T load (Key key, CacheStrategy strategy, - QueryClause... clauses) - throws DatabaseException - { - clauses = ArrayUtil.append(clauses, key); - return _ctx.invoke(new FindOneQuery(_ctx, key.getPersistentClass(), strategy, clauses)); - } - - /** - * Loads the first persistent object that matches the supplied query clauses. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected T load (Class type, QueryClause... clauses) - throws DatabaseException - { - return load(type, CacheStrategy.BEST, clauses); - } - - /** - * Loads the first persistent object that matches the supplied query clauses. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected T load (Class type, CacheStrategy strategy, - QueryClause... clauses) - throws DatabaseException - { - return _ctx.invoke(new FindOneQuery(_ctx, type, strategy, clauses)); - } - - /** - * Loads up all persistent records that match the supplied set of raw primary keys. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList loadAll ( - Class type, Collection> primaryKeys) - throws DatabaseException - { - // convert the raw keys into real key records - DepotMarshaller marsh = _ctx.getMarshaller(type); - List> keys = Lists.newArrayList(); - for (Comparable key : primaryKeys) { - keys.add(marsh.makePrimaryKey(key)); - } - return loadAll(keys); - } - - /** - * Loads up all persistent records that match the supplied set of primary keys. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList loadAll (Collection> keys) - throws DatabaseException - { - return (keys.size() == 0) ? new XArrayList() : - _ctx.invoke(new FindAllQuery.WithKeys(_ctx, keys)); - } - - /** - * A varargs version of {@link #findAll(Class,Collection)}. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList findAll (Class type, QueryClause... clauses) - throws DatabaseException - { - return findAll(type, Arrays.asList(clauses)); - } - - /** - * Loads all persistent objects that match the specified clauses. - * - * We have two strategies for doing this: one performs the query as-is, the second executes two - * passes: first fetching only key columns and consulting the cache for each such key; then, in - * the second pass, fetching the full entity only for keys that were not found in the cache. - * - * The more complex strategy could save a lot of data shuffling. On the other hand, its - * complexity is an inherent drawback, and it does execute two separate database queries for - * what the simple method does in one. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList findAll ( - Class type, Collection clauses) - throws DatabaseException - { - return findAll(type, CacheStrategy.BEST, clauses); - } - - /** - * A varargs version of {@link #findAll(Class,CacheStrategy,Collection)}. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList findAll ( - Class type, CacheStrategy strategy, QueryClause... clauses) - throws DatabaseException - { - return findAll(type, strategy, Arrays.asList(clauses)); - } - - /** - * Loads all persistent objects that match the specified clauses. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList findAll ( - Class type, CacheStrategy cache, Collection clauses) - throws DatabaseException - { - DepotMarshaller marsh = _ctx.getMarshaller(type); - - switch (cache) { - case LONG_KEYS: case SHORT_KEYS: case BEST: case RECORDS: - String reason = null; - if (marsh.getTableName() == null) { - reason = type + " is computed"; - - } else if (!marsh.hasPrimaryKey()) { - reason = type + " has no primary key"; - - } else { - for (QueryClause clause : clauses) { - if (clause instanceof FieldOverride) { - reason = "query uses a FieldOverride clause"; - break; - } - } - } - if (cache == CacheStrategy.BEST) { - cache = (reason != null) ? CacheStrategy.NONE : CacheStrategy.SHORT_KEYS; - - } else if (reason != null) { - // if user explicitly asked for a strategy we can't do, protest - throw new IllegalArgumentException( - "Cannot use " + cache + " strategy because " + reason); - } - break; - - case NONE: case CONTENTS: - break; // NONE and CONTENTS can always be used. - } - - if (!_ctx.isUsingCache()) { - cache = CacheStrategy.NONE; - } - - switch (cache) { - case SHORT_KEYS: case LONG_KEYS: case RECORDS: - return _ctx.invoke(new FindAllQuery.WithCache(_ctx, type, clauses, cache)); - - default: - return _ctx.invoke(new FindAllQuery.Explicitly( - _ctx, type, clauses, cache == CacheStrategy.CONTENTS)); - } - } - - /** - * Looks up and returns {@link Key} records for all rows that match the supplied query clauses. - * - * @param forUpdate if true, the query will be run using a read-write connection to ensure that - * it talks to the master database, if false, the query will be run on a read-only connection - * and may load keys from a slave. For performance reasons, you should always pass false unless - * you know you will be modifying the database as a result of this query and absolutely need - * the latest data. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList> findAllKeys ( - Class type, boolean forUpdate, QueryClause... clause) - throws DatabaseException - { - return findAllKeys(type, forUpdate, Arrays.asList(clause)); - } - - /** - * Looks up and returns {@link Key} records for all rows that match the supplied query clauses. - * - * @param forUpdate if true, the query will be run using a read-write connection to ensure that - * it talks to the master database, if false, the query will be run on a read-only connection - * and may load keys from a slave. For performance reasons, you should always pass false unless - * you know you will be modifying the database as a result of this query and absolutely need - * the latest data. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected XList> findAllKeys ( - Class type, boolean forUpdate, Collection clauses) - throws DatabaseException - { - return _ctx.invoke(new FindAllKeysQuery(_ctx, type, forUpdate, clauses)); - } - - /** - * Inserts the supplied persistent object into the database, assigning its primary key (if it - * has one) in the process. - * - * @return the number of rows modified by this action, this should always be one. - * - * @throws DuplicateKeyException if the inserted record conflicts with the primary key (or any - * other unique key) of a record already in the database. - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int insert (T record) - throws DatabaseException - { - @SuppressWarnings("unchecked") final Class pClass = (Class) record.getClass(); - final DepotMarshaller marsh = _ctx.getMarshaller(pClass); - Key key = marsh.getPrimaryKey(record, false); - - DepotTypes types = DepotTypes.getDepotTypes(_ctx); - types.addClass(_ctx, pClass); - final SQLBuilder builder = _ctx.getSQLBuilder(types); - - // key will be null if record was supplied without a primary key - return _ctx.invoke(new CachingModifier(record, key, key) { - @Override - protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - // if needed, update our modifier's key so that it can cache our results - Set identityFields = Collections.emptySet(); - if (_key == null) { - // set any auto-generated column values - identityFields = marsh.generateFieldValues(conn, liaison, _result, false); - updateKey(marsh.getPrimaryKey(_result, false)); - } - - builder.newQuery(new InsertClause(pClass, _result, identityFields)); - - int mods = builder.prepare(conn).executeUpdate(); - // run any post-factum value generators and potentially generate our key - if (_key == null) { - marsh.generateFieldValues(conn, liaison, _result, true); - updateKey(marsh.getPrimaryKey(_result, false)); - } - return mods; - } - }); - } - - /** - * Updates all fields of the supplied persistent object, using its primary key to identify the - * row to be updated. - * - * @return the number of rows modified by this action. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int update (PersistentRecord record) - throws DatabaseException - { - Class pClass = record.getClass(); - requireNotComputed(pClass, "update"); - DepotMarshaller marsh = _ctx.getMarshaller(pClass); - Key key = marsh.getPrimaryKey(record); - if (key == null) { - throw new IllegalArgumentException("Can't update record with null primary key."); - } - return doUpdate(key, new UpdateClause(pClass, key, marsh.getColumnFieldNames(), record)); - } - - /** - * Updates just the specified fields of the supplied persistent object, using its primary key - * to identify the row to be updated. This method currently flushes the associated record from - * the cache, but in the future it should be modified to update the modified fields in the - * cached value iff the record exists in the cache. - * - * @return the number of rows modified by this action. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int update (T record, final ColumnExp... modifiedFields) - throws DatabaseException - { - @SuppressWarnings("unchecked") Class pClass = (Class) record.getClass(); - requireNotComputed(pClass, "update"); - DepotMarshaller marsh = _ctx.getMarshaller(pClass); - Key key = marsh.getPrimaryKey(record); - if (key == null) { - throw new IllegalArgumentException("Can't update record with null primary key."); - } - return doUpdate(key, new UpdateClause(pClass, key, modifiedFields, record)); - } - - /** - * Updates the specified columns for all persistent objects matching the supplied key. - * - * @param key the key for the persistent objects to be modified. - * @param field the first field to be updated. - * @param value the value to assign to the first field. This may be a primitive (Integer, - * String, etc.) which will be wrapped in value expression or a SQLExpression instance. - * @param more additional (field, value) pairs to be updated. - * - * @return the number of rows modified by this action. - * - * @throws DuplicateKeyException if the update attempts to change the key columns of a row to - * values that duplicate another row already in the database. - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int updatePartial ( - Key key, ColumnExp field, Object value, Object... more) - throws DatabaseException - { - return updatePartial(key.getPersistentClass(), key, key, field, value, more); - } - - /** - * Updates the specified columns for all persistent objects matching the supplied key. - * - * @param key the key for the persistent objects to be modified. - * @param updates a mapping from field to value for all values to be changed. The values may be - * primitives (Integer, String, etc.) which will be wrapped in value expression instances or - * SQLExpression instances defining the value. - * - * @return the number of rows modified by this action. - * - * @throws DuplicateKeyException if the update attempts to change the key columns of a row to - * values that duplicate another row already in the database. - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int updatePartial (Key key, Map updates) - throws DatabaseException - { - return updatePartial(key.getPersistentClass(), key, key, updates); - } - - /** - * Updates the specified columns for all persistent objects matching the supplied key. This - * method currently flushes the associated record from the cache, but in the future it should - * be modified to update the modified fields in the cached value iff the record exists in the - * cache. - * - * @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 run prior to the update to flush the - * relevant persistent objects from the cache, or null if no invalidation is needed. - * @param updates a mapping from field to value for all values to be changed. The values may be - * primitives (Integer, String, etc.) which will be wrapped in value expression instances or - * SQLExpression instances defining the value. - * - * @return the number of rows modified by this action. - * - * @throws DuplicateKeyException if the update attempts to change the key columns of a row to - * values that duplicate another row already in the database. - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int updatePartial ( - Class type, final WhereClause key, CacheInvalidator invalidator, - Map updates) - throws DatabaseException - { - // separate the arguments into keys and values - final ColumnExp[] fields = new ColumnExp[updates.size()]; - final SQLExpression[] values = new SQLExpression[fields.length]; - int ii = 0; - for (Map.Entry entry : updates.entrySet()) { - fields[ii] = entry.getKey(); - values[ii++] = makeValue(entry.getValue()); - } - return updatePartial(type, key, invalidator, fields, values); - } - - /** - * Updates the specified columns for all persistent objects matching the supplied key. This - * method currently flushes the associated record from the cache, but in the future it should - * be modified to update the modified fields in the cached value iff the record exists in the - * cache. - * - * @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 run prior to the update to flush the - * relevant persistent objects from the cache, or null if no invalidation is needed. - * @param field the first field to be updated. - * @param value the value to assign to the first field. This may be a primitive (Integer, - * String, etc.) which will be wrapped in value expression or a SQLExpression instance. - * @param more additional (field, value) pairs to be updated. - * - * @return the number of rows modified by this action. - * - * @throws DuplicateKeyException if the update attempts to change the key columns of a row to - * values that duplicate another row already in the database. - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int updatePartial ( - Class type, final WhereClause key, CacheInvalidator invalidator, - ColumnExp field, Object value, Object... more) - throws DatabaseException - { - // separate the updates into keys and values - final ColumnExp[] fields = new ColumnExp[1+more.length/2]; - final SQLExpression[] values = new SQLExpression[fields.length]; - fields[0] = field; - values[0] = makeValue(value); - for (int ii = 1, idx = 0; ii < fields.length; ii++) { - fields[ii] = (ColumnExp)more[idx++]; - values[ii] = makeValue(more[idx++]); - } - return updatePartial(type, key, invalidator, fields, values); - } - - /** - * Updates the specified columns for all persistent objects matching the supplied key. This - * method currently flushes the associated record from the cache, but in the future it should - * be modified to update the modified fields in the cached value iff the record exists in the - * cache. - * - * @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 run prior to the update to flush the - * relevant persistent objects from the cache, or null if no invalidation is needed. - * @param fields the fields in the objects to be updated. - * @param values the values to be assigned to the fields. - * - * @return the number of rows modified by this action. - * - * @throws DuplicateKeyException if the update attempts to change the key columns of a row to - * values that duplicate another row already in the database. - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int updatePartial ( - Class type, final WhereClause key, CacheInvalidator invalidator, - ColumnExp[] fields, SQLExpression[] values) - throws DatabaseException - { - requireNotComputed(type, "updatePartial"); - if (invalidator instanceof ValidatingCacheInvalidator) { - ((ValidatingCacheInvalidator)invalidator).validateFlushType(type); // sanity check - } - key.validateQueryType(type); // and another - return doUpdate(invalidator, new UpdateClause(type, key, fields, values)); - } - - /** - * Stores the supplied persisent object in the database. If it has no primary key assigned (it - * is null or zero), it will be inserted directly. Otherwise an update will first be attempted - * and if that matches zero rows, the object will be inserted. - * - * @return true if the record was created, false if it was updated. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected boolean store (T record) - throws DatabaseException - { - @SuppressWarnings("unchecked") final Class pClass = (Class) record.getClass(); - requireNotComputed(pClass, "store"); - - final DepotMarshaller marsh = _ctx.getMarshaller(pClass); - Key key = marsh.hasPrimaryKey() ? marsh.getPrimaryKey(record) : null; - final UpdateClause update = - new UpdateClause(pClass, key, marsh.getColumnFieldNames(), record); - final SQLBuilder builder = _ctx.getSQLBuilder(DepotTypes.getDepotTypes(_ctx, update)); - - // if our primary key isn't null, we start by trying to update rather than insert - if (key != null) { - builder.newQuery(update); - } - - final boolean[] created = new boolean[1]; - _ctx.invoke(new CachingModifier(record, key, key) { - @Override - protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - if (_key != null) { - // run the update - int mods = builder.prepare(conn).executeUpdate(); - if (mods > 0) { - // if it succeeded, we're done - return mods; - } - } - - // if the update modified zero rows or the primary key was unset, insert - Set identityFields = Collections.emptySet(); - if (_key == null) { - - // first, set any auto-generated column values - identityFields = marsh.generateFieldValues(conn, liaison, _result, false); - // update our modifier's key so that it can cache our results - updateKey(marsh.getPrimaryKey(_result, false)); - } - - builder.newQuery(new InsertClause(pClass, _result, identityFields)); - - int mods = builder.prepare(conn).executeUpdate(); - - // run any post-factum value generators and potentially generate our key - if (_key == null) { - marsh.generateFieldValues(conn, liaison, _result, true); - updateKey(marsh.getPrimaryKey(_result, false)); - } - created[0] = true; - return mods; - } - }); - return created[0]; - } - - /** - * Deletes all persistent objects from the database matching the primary key of the supplied - * object (which should be one or zero). - * - * @return the number of rows deleted by this action. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int delete (T record) - throws DatabaseException - { - @SuppressWarnings("unchecked") Class type = (Class)record.getClass(); - Key primaryKey = _ctx.getMarshaller(type).getPrimaryKey(record); - if (primaryKey == null) { - throw new IllegalArgumentException("Can't delete record with null primary key."); - } - return delete(primaryKey); - } - - /** - * Deletes all persistent objects from the database matching the supplied primary key (which - * should be one or zero). - * - * @return the number of rows deleted by this action. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int delete (Key primaryKey) - throws DatabaseException - { - return deleteAll(primaryKey.getPersistentClass(), primaryKey, primaryKey); - } - - /** - * Deletes all persistent objects from the database that match the supplied where clause. - * - * @return the number of rows deleted by this action. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int deleteAll (Class type, final WhereClause where) - throws DatabaseException - { - if (where instanceof CacheInvalidator) { - // our where clause knows how to do its own deletion, yay! - return deleteAll(type, where, (CacheInvalidator)where); - } else if (_ctx.getMarshaller(type).hasPrimaryKey()) { - // look up the primary keys for all matching rows matching and delete using those - KeySet pwhere = KeySet.newKeySet(type, findAllKeys(type, true, where)); - return deleteAll(type, pwhere, pwhere); - } else { - // otherwise just do the delete directly as we can't have cached a record that has no - // primary key in the first place - return deleteAll(type, where, null); - } - } - - /** - * Deletes all persistent objects from the database that match the supplied key. - * - * @return the number of rows deleted by this action. - * - * @throws DatabaseException if any problem is encountered communicating with the database. - */ - protected int deleteAll ( - Class type, final WhereClause where, CacheInvalidator invalidator) - throws DatabaseException - { - if (invalidator instanceof ValidatingCacheInvalidator) { - ((ValidatingCacheInvalidator)invalidator).validateFlushType(type); // sanity check - } - where.validateQueryType(type); // and another - - DeleteClause delete = new DeleteClause(type, where); - final SQLBuilder builder = _ctx.getSQLBuilder(DepotTypes.getDepotTypes(_ctx, delete)); - builder.newQuery(delete); - - return _ctx.invoke(new Modifier(invalidator) { - @Override - protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - return builder.prepare(conn).executeUpdate(); - } - }); - } - // make sure the given type corresponds to a concrete class protected void requireNotComputed (Class type, String action) throws DatabaseException