From 09d1185ab73a0e3414add4a5bd77f426807226bd Mon Sep 17 00:00:00 2001 From: Kevin Wise Date: Fri, 8 Apr 2022 15:33:33 -0700 Subject: [PATCH] Add support for add and update --- .../com/googlecode/objectify/Objectify.java | 34 ++++- .../objectify/impl/AddSaverImpl.java | 25 ++++ .../impl/AsyncDatastoreReaderWriter.java | 6 +- .../impl/AsyncDatastoreReaderWriterImpl.java | 27 +++- .../objectify/impl/ObjectifyImpl.java | 22 +++- .../objectify/impl/PutSaverImpl.java | 25 ++++ .../googlecode/objectify/impl/SaverImpl.java | 10 +- .../objectify/impl/UpdateSaverImpl.java | 25 ++++ .../objectify/impl/WriteEngine.java | 118 +++++++++++++++--- 9 files changed, 261 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/googlecode/objectify/impl/AddSaverImpl.java create mode 100644 src/main/java/com/googlecode/objectify/impl/PutSaverImpl.java create mode 100644 src/main/java/com/googlecode/objectify/impl/UpdateSaverImpl.java diff --git a/src/main/java/com/googlecode/objectify/Objectify.java b/src/main/java/com/googlecode/objectify/Objectify.java index ab69ac301..b2e1aca67 100644 --- a/src/main/java/com/googlecode/objectify/Objectify.java +++ b/src/main/java/com/googlecode/objectify/Objectify.java @@ -36,8 +36,40 @@ public interface Objectify */ Loader load(); + /** + *

Start an add command chain. Allows you to add entity objects (datastore.add). Note that all command + * chain objects are immutable.

+ * + *

Saves do NOT cascade; if you wish to save an object graph, you must save each individual entity.

+ * + *

A quick example: + * {@code ofy().add().entities(e1, e2, e3).now();}

+ * + *

All command objects are immutable; this method returns a new object rather than modifying the + * current command object.

+ * + * @return the next step in the immutable command chain. + */ + Saver add(); + + /** + *

Start an update command chain. Allows you to update (re-save) entity objects (datastore.update). Note that all command + * chain objects are immutable.

+ * + *

Saves do NOT cascade; if you wish to save an object graph, you must save each individual entity.

+ * + *

A quick example: + * {@code ofy().update().entities(e1, e2, e3).now();}

+ * + *

All command objects are immutable; this method returns a new object rather than modifying the + * current command object.

+ * + * @return the next step in the immutable command chain. + */ + Saver update(); + /** - *

Start a save command chain. Allows you to save (or re-save) entity objects. Note that all command + *

Start a save command chain. Allows you to save (or re-save) entity objects (datastore.put). Note that all command * chain objects are immutable.

* *

Saves do NOT cascade; if you wish to save an object graph, you must save each individual entity.

diff --git a/src/main/java/com/googlecode/objectify/impl/AddSaverImpl.java b/src/main/java/com/googlecode/objectify/impl/AddSaverImpl.java new file mode 100644 index 000000000..d36f2350f --- /dev/null +++ b/src/main/java/com/googlecode/objectify/impl/AddSaverImpl.java @@ -0,0 +1,25 @@ +package com.googlecode.objectify.impl; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.Result; +import java.util.Map; + +/** + * Implementation of the Add/Create in datastore + */ +public class AddSaverImpl extends SaverImpl { + + /** */ + public AddSaverImpl(ObjectifyImpl ofy) { + super(ofy); + } + + /* (non-Javadoc) + * @see com.googlecode.objectify.cmd.Saver#entities(java.lang.Iterable) + */ + @Override + public Result, E>> entities(final Iterable entities) { + return ofy.createWriteEngine().create(entities); + } + +} diff --git a/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriter.java b/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriter.java index 5432e4d41..3f2d83b58 100644 --- a/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriter.java +++ b/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriter.java @@ -29,6 +29,10 @@ public interface AsyncDatastoreReaderWriter { Future> put(final Iterable> entities); + Future> add(final Iterable> entities); + + Future update(final Iterable entities); + default Future> get(final Key... keys) { return get(Arrays.asList(keys)); } @@ -38,4 +42,4 @@ default Future> put(final FullEntity... entities) { } Future runAggregation(final AggregationQuery query); -} \ No newline at end of file +} diff --git a/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriterImpl.java b/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriterImpl.java index 670a14393..e13b94d6b 100644 --- a/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriterImpl.java +++ b/src/main/java/com/googlecode/objectify/impl/AsyncDatastoreReaderWriterImpl.java @@ -87,9 +87,34 @@ public Future> put(final Iterable> entities) { return new FutureNow<>(result); } + @Override + public Future> add(final Iterable> entities) { + final Iterable>> partitions = Iterables.partition(entities, MAX_WRITE_SIZE); + + final List result = new ArrayList<>(); + + for (final List> partition : partitions) { + final List saved = datastoreReaderWriter.put(Iterables.toArray(partition, FullEntity.class)); + saved.stream().map(Entity::getKey).forEach(result::add); + } + + return new FutureNow<>(result); + } + + @Override + public Future update(final Iterable entities) { + final Iterable> partitions = Iterables.partition(entities, MAX_WRITE_SIZE); + + for (final List partition : partitions) { + datastoreReaderWriter.update(Iterables.toArray(partition, Entity.class)); + } + + return new FutureNow<>(null); + } + @Override public Future runAggregation(final AggregationQuery query) { final AggregationResults results = datastoreReaderWriter.runAggregation(query); return new FutureNow<>(results); } -} \ No newline at end of file +} diff --git a/src/main/java/com/googlecode/objectify/impl/ObjectifyImpl.java b/src/main/java/com/googlecode/objectify/impl/ObjectifyImpl.java index 2392db12a..cf858f026 100644 --- a/src/main/java/com/googlecode/objectify/impl/ObjectifyImpl.java +++ b/src/main/java/com/googlecode/objectify/impl/ObjectifyImpl.java @@ -83,11 +83,27 @@ public Loader load() { } /* (non-Javadoc) - * @see com.googlecode.objectify.Objectify#put() + * @see com.googlecode.objectify.Objectify#add() + */ + @Override + public Saver add() { + return new AddSaverImpl(this); + } + + /* (non-Javadoc) + * @see com.googlecode.objectify.Objectify#update() + */ + @Override + public Saver update() { + return new UpdateSaverImpl(this); + } + + /* (non-Javadoc) + * @see com.googlecode.objectify.Objectify#save() */ @Override public Saver save() { - return new SaverImpl(this); + return new PutSaverImpl(this); } /* (non-Javadoc) @@ -356,4 +372,4 @@ public void close() { factory().close(this); } -} \ No newline at end of file +} diff --git a/src/main/java/com/googlecode/objectify/impl/PutSaverImpl.java b/src/main/java/com/googlecode/objectify/impl/PutSaverImpl.java new file mode 100644 index 000000000..01d5ba26e --- /dev/null +++ b/src/main/java/com/googlecode/objectify/impl/PutSaverImpl.java @@ -0,0 +1,25 @@ +package com.googlecode.objectify.impl; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.Result; +import java.util.Map; + +/** + * Implementation of the Put interface. + */ +public class PutSaverImpl extends SaverImpl { + + /** */ + public PutSaverImpl(final ObjectifyImpl ofy) { + super(ofy); + } + + /* (non-Javadoc) + * @see com.googlecode.objectify.cmd.Saver#entities(java.lang.Iterable) + */ + @Override + public Result, E>> entities(final Iterable entities) { + return ofy.createWriteEngine().save(entities); + } + +} diff --git a/src/main/java/com/googlecode/objectify/impl/SaverImpl.java b/src/main/java/com/googlecode/objectify/impl/SaverImpl.java index 8f31c906e..5d901860d 100644 --- a/src/main/java/com/googlecode/objectify/impl/SaverImpl.java +++ b/src/main/java/com/googlecode/objectify/impl/SaverImpl.java @@ -13,14 +13,14 @@ /** - * Implementation of the Put interface. + * Implementation of the Saver interface. * * @author Jeff Schnitzer */ -public class SaverImpl implements Saver +public abstract class SaverImpl implements Saver { /** */ - private final ObjectifyImpl ofy; + protected final ObjectifyImpl ofy; /** */ public SaverImpl(final ObjectifyImpl ofy) { @@ -56,9 +56,7 @@ public Result, E>> entities(final E... entities) { * @see com.googlecode.objectify.cmd.Saver#entities(java.lang.Iterable) */ @Override - public Result, E>> entities(final Iterable entities) { - return ofy.createWriteEngine().save(entities); - } + abstract public Result, E>> entities(final Iterable entities); /* (non-Javadoc) * @see com.googlecode.objectify.cmd.Saver#toEntity(java.lang.Object) diff --git a/src/main/java/com/googlecode/objectify/impl/UpdateSaverImpl.java b/src/main/java/com/googlecode/objectify/impl/UpdateSaverImpl.java new file mode 100644 index 000000000..e2d983701 --- /dev/null +++ b/src/main/java/com/googlecode/objectify/impl/UpdateSaverImpl.java @@ -0,0 +1,25 @@ +package com.googlecode.objectify.impl; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.Result; +import java.util.Map; + +/** + * Implementation of the Update in datastore + */ +public class UpdateSaverImpl extends SaverImpl { + + /** */ + public UpdateSaverImpl(ObjectifyImpl ofy) { + super(ofy); + } + + /* (non-Javadoc) + * @see com.googlecode.objectify.cmd.Saver#entities(java.lang.Iterable) + */ + @Override + public Result, E>> entities(final Iterable entities) { + return ofy.createWriteEngine().update(entities); + } + +} diff --git a/src/main/java/com/googlecode/objectify/impl/WriteEngine.java b/src/main/java/com/googlecode/objectify/impl/WriteEngine.java index c59a44935..6876d95eb 100644 --- a/src/main/java/com/googlecode/objectify/impl/WriteEngine.java +++ b/src/main/java/com/googlecode/objectify/impl/WriteEngine.java @@ -1,5 +1,6 @@ package com.googlecode.objectify.impl; +import com.google.cloud.datastore.Entity; import com.google.cloud.datastore.FullEntity; import com.google.common.collect.Lists; import com.googlecode.objectify.Key; @@ -9,14 +10,14 @@ import com.googlecode.objectify.impl.translate.SaveContext; import com.googlecode.objectify.util.Closeable; import com.googlecode.objectify.util.ResultWrapper; -import lombok.extern.slf4j.Slf4j; - import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Future; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; /** * This is the master logic for saving and deleting entities from the datastore. It provides the @@ -49,21 +50,32 @@ public WriteEngine(ObjectifyImpl ofy, AsyncDatastoreReaderWriter datastore, Sess this.deferrer = deferrer; } - /** - * The fundamental put() operation. - */ - public Result, E>> save(Iterable entities) { - log.trace("Saving {}", entities); - - // A hacky way of doing this but otherwise we have to adjust the save() contracts to take a namespace - final Closeable unsetNamespace = ofy.getOptions().getNamespace() == null ? null : NamespaceManager.set(ofy.getOptions().getNamespace()); + /** + * The fundamental add() operation. + */ + public Result, E>> create(Iterable entities) { + log.trace("Creating {}", entities); + return processCreateOrSave(entities, datastore::add, "create", "Created"); + } + + /** + * The fundamental put() operation. + */ + public Result, E>> save(Iterable entities) { + log.trace("Saving {}", entities); + return processCreateOrSave(entities, datastore::put, "save", "Saved"); + } + + private Result, E>> processCreateOrSave(Iterable entities, Function>, Future>> operation, String operationVerb, String operationVerbPastTense) { + // A hacky way of doing this but otherwise we have to adjust the save()/create() contracts to take a namespace + final Closeable unsetNamespace = ofy.getOptions().getNamespace() == null ? null : NamespaceManager.set(ofy.getOptions().getNamespace()); try { final SaveContext ctx = new SaveContext(); final List> entityList = new ArrayList<>(); for (final E obj : entities) { if (obj == null) - throw new NullPointerException("Attempted to save a null entity"); + throw new NullPointerException("Attempted to "+ operationVerb + " a null entity"); deferrer.undefer(ofy.getOptions(), obj); @@ -80,7 +92,7 @@ public Result, E>> save(Iterable entities) { final List original = Lists.newArrayList(entities); // The CachingDatastoreService needs its own raw transaction - final Future> raw = datastore.put(entityList); + final Future> raw = operation.apply(entityList); final Result> adapted = new ResultAdapter<>(raw); final Result, E>> result = new ResultWrapper, Map, E>>(adapted) { @@ -108,7 +120,7 @@ protected Map, E> wrap(List base) { session.addValue(key, obj); } - log.trace("Saved {}", base); + log.trace("{} {}", operationVerbPastTense, base); return result; } @@ -122,11 +134,79 @@ protected Map, E> wrap(List base) { if (unsetNamespace != null) unsetNamespace.close(); } - } - - private ObjectifyFactory factory() { - return ofy.factory(); - } + } + + /** + * The fundamental update() operation. + */ + public Result, E>> update(Iterable entities) { + // A hacky way of doing this but otherwise we have to adjust the update() contracts to take a namespace + final String namespace = ofy.getOptions().getNamespace(); + try (Closeable ignored = namespace == null ? null : NamespaceManager.set(namespace)) { + final SaveContext ctx = new SaveContext(); + + final List entityList = new ArrayList<>(); + final Map, E> entityMap = new LinkedHashMap<>(); + + for (final E obj : entities) { + if (obj == null) + throw new NullPointerException("Attempted to update a null entity"); + + final Entity entity; + final com.google.cloud.datastore.Key key; + if (obj instanceof Entity) { + entity = (Entity) obj; + key = entity.getKey(); + } else { + + final com.google.cloud.datastore.IncompleteKey incompleteKey = + factory().keys().getMetadataSafe(obj).getIncompleteKey(obj, namespace); + + if (incompleteKey instanceof com.google.cloud.datastore.Key) { + key = (com.google.cloud.datastore.Key) incompleteKey; + } else { + throw new IllegalStateException( + "You cannot update an object with a null @Id. Object was " + obj); + } + + deferrer.undefer(ofy.getOptions(), obj); + + if (obj instanceof FullEntity) { + entity = Entity.newBuilder(key, (FullEntity) obj).build(); + } else { + final FullEntity translated = factory().getMetadataForEntity(obj).save(obj, ctx); + entity = Entity.newBuilder(key, translated).build(); + } + } + + entityList.add(entity); + entityMap.put(Key.create(key), obj); + } + + final Future fut = datastore.update(entityList); + final Result adapted = new ResultAdapter<>(fut); + final Result, E>> result = new ResultWrapper, E>>(adapted) { + @Override + protected Map, E> wrap(final Void orig) { + for (Map.Entry, E> entry : entityMap.entrySet()) + session.addValue(entry.getKey(), entry.getValue()); + + log.trace("Updated {}", entityMap.keySet()); + + return entityMap; + } + }; + + if (ofy.getTransaction() != null) + ((PrivateAsyncTransaction) ofy.getTransaction()).enlist(result); + + return result; + } + } + + private ObjectifyFactory factory() { + return ofy.factory(); + } /** * The fundamental delete() operation. @@ -152,4 +232,4 @@ protected Void wrap(final Void orig) { return result; } -} \ No newline at end of file +}