Skip to content

Commit 6f85415

Browse files
feat(datastore) only include changed fields in update mutations (#1110)
1 parent 84c9cf5 commit 6f85415

File tree

11 files changed

+293
-29
lines changed

11 files changed

+293
-29
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.datastore.appsync;
17+
18+
import com.amplifyframework.core.model.Model;
19+
import com.amplifyframework.util.GsonFactory;
20+
import com.amplifyframework.util.GsonObjectConverter;
21+
22+
import com.google.gson.Gson;
23+
import com.google.gson.JsonElement;
24+
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
28+
/**
29+
* Utility for converting a Model to/from a Map<String, Object>.
30+
*/
31+
public final class ModelConverter {
32+
33+
private ModelConverter() {}
34+
35+
/**
36+
* Convert a Model to a Map<String, Object>.
37+
* @param model a Model instance.
38+
* @param <T> type of the Model instance.
39+
* @return a Map&lt;String, Object&gt; representation of the provided Model instance.
40+
*/
41+
public static <T extends Model> Map<String, Object> toMap(T model) {
42+
if (model == null) {
43+
return new HashMap<>();
44+
}
45+
if (model instanceof SerializedModel) {
46+
return ((SerializedModel) model).getSerializedData();
47+
} else {
48+
Gson gson = GsonFactory.instance();
49+
JsonElement jsonElement = gson.toJsonTree(model);
50+
return GsonObjectConverter.toMap(jsonElement.getAsJsonObject());
51+
}
52+
}
53+
54+
/**
55+
* Convert a Map&lt;String, Object&gt; to a Model.
56+
* @param map a Map&lt;String, Object&gt;.
57+
* @param modelClass Class of Model to convert the Map to.
58+
* @param <T> type of the Model instance to convert to.
59+
* @return a Model instance created from the provided Map.
60+
*/
61+
public static <T extends Model> T fromMap(Map<String, Object> map, Class<T> modelClass) {
62+
Gson gson = GsonFactory.instance();
63+
String jsonString = gson.toJson(map);
64+
return gson.fromJson(jsonString, modelClass);
65+
}
66+
}

aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedModel.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,45 @@ private SerializedModel(
4444
this.modelSchema = modelSchema;
4545
}
4646

47+
/**
48+
* Creates a SerializedModel from a generated Java Model object.
49+
* @param model Model object
50+
* @param modelSchema schema for the Model object
51+
* @param <T> type of the Model object.
52+
* @return SerializedModel equivalent of the Model object.
53+
*/
54+
public static <T extends Model> SerializedModel create(T model, ModelSchema modelSchema) {
55+
return SerializedModel.builder()
56+
.serializedData(ModelConverter.toMap(model))
57+
.modelSchema(modelSchema)
58+
.build();
59+
}
60+
61+
/**
62+
* Computes the difference between two Models, comparing equality of each field value for each Model, and returns
63+
* the difference as a SerializedModel.
64+
* @param updated the updated Model, whose values will be used to build the resulting SerializedModel.
65+
* @param original the original Model to compare against.
66+
* @param modelSchema ModelSchema for the Models between compared.
67+
* @param <T> type of the Models being compared.
68+
* @return a SerializedModel, containing only the values from the updated Model that are different from the
69+
* corresponding values in original.
70+
*/
71+
public static <T extends Model> SerializedModel difference(T updated, T original, ModelSchema modelSchema) {
72+
Map<String, Object> updatedMap = ModelConverter.toMap(updated);
73+
Map<String, Object> originalMap = ModelConverter.toMap(original);
74+
Map<String, Object> patchMap = new HashMap<>();
75+
for (String key : updatedMap.keySet()) {
76+
if ("id".equals(key) || !ObjectsCompat.equals(originalMap.get(key), updatedMap.get(key))) {
77+
patchMap.put(key, updatedMap.get(key));
78+
}
79+
}
80+
return SerializedModel.builder()
81+
.serializedData(patchMap)
82+
.modelSchema(modelSchema)
83+
.build();
84+
}
85+
4786
/**
4887
* Return a builder of {@link SerializedModel}.
4988
* @return A serialized model builder

aws-datastore/src/androidTest/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapterSaveTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717

1818
import android.util.Log;
1919

20+
import com.amplifyframework.AmplifyException;
21+
import com.amplifyframework.core.model.Model;
22+
import com.amplifyframework.core.model.ModelSchema;
2023
import com.amplifyframework.core.model.query.predicate.QueryPredicate;
2124
import com.amplifyframework.datastore.DataStoreException;
2225
import com.amplifyframework.datastore.StrictMode;
26+
import com.amplifyframework.datastore.appsync.SerializedModel;
27+
import com.amplifyframework.datastore.storage.StorageItemChange;
2328
import com.amplifyframework.datastore.storage.SynchronousStorageAdapter;
2429
import com.amplifyframework.testmodels.commentsblog.AmplifyModelProvider;
2530
import com.amplifyframework.testmodels.commentsblog.Blog;
@@ -30,10 +35,14 @@
3035
import org.junit.BeforeClass;
3136
import org.junit.Test;
3237

38+
import java.util.HashMap;
3339
import java.util.HashSet;
3440
import java.util.List;
41+
import java.util.Map;
42+
import java.util.concurrent.TimeUnit;
3543

3644
import io.reactivex.rxjava3.core.Observable;
45+
import io.reactivex.rxjava3.observers.TestObserver;
3746

3847
import static org.hamcrest.CoreMatchers.containsString;
3948
import static org.hamcrest.MatcherAssert.assertThat;
@@ -276,4 +285,41 @@ public void saveModelWithPredicateUpdatesConditionally() throws DataStoreExcepti
276285
.blockingGet()
277286
);
278287
}
288+
289+
/**
290+
* Verify that saving an item that already exists emits a StorageItemChange event with a patchItem that only
291+
* contains the fields that are different.
292+
*
293+
* @throws AmplifyException On failure to obtain ModelSchema from model class.
294+
* @throws InterruptedException If interrupted while awaiting terminal result in test observer
295+
*/
296+
@Test
297+
public void patchItemOnlyHasChangedFields() throws AmplifyException, InterruptedException {
298+
// Create a BlogOwner.
299+
final BlogOwner johnSmith = BlogOwner.builder()
300+
.name("John Smith")
301+
.wea("ther")
302+
.build();
303+
adapter.save(johnSmith);
304+
305+
// Start observing for changes
306+
TestObserver<StorageItemChange<? extends Model>> observer = adapter.observe().test();
307+
308+
// Update one field on the BlogOwner.
309+
BlogOwner johnAdams = johnSmith.copyOfBuilder().name("John Adams").build();
310+
adapter.save(johnAdams);
311+
312+
// Observe that the StorageItemChange contains an item with only the fields that changed (`id`, and `name`, but
313+
// not `wea`)
314+
Map<String, Object> serializedData = new HashMap<>();
315+
serializedData.put("id", johnAdams.getId());
316+
serializedData.put("name", "John Adams");
317+
SerializedModel expectedItem = SerializedModel.builder()
318+
.serializedData(serializedData)
319+
.modelSchema(ModelSchema.fromModelClass(BlogOwner.class))
320+
.build();
321+
observer.await(1, TimeUnit.SECONDS);
322+
observer.assertValueCount(1);
323+
observer.assertValueAt(0, storageItemChange -> storageItemChange.patchItem().equals(expectedItem));
324+
}
279325
}

aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,14 @@ private static Map<String, Object> extractFieldLevelData(
341341
try {
342342
final ModelAssociation association = schema.getAssociations().get(fieldName);
343343
if (association == null) {
344-
result.put(fieldName, extractFieldValue(modelField, instance));
344+
if (instance instanceof SerializedModel) {
345+
Map<String, Object> serializedData = ((SerializedModel) instance).getSerializedData();
346+
if (serializedData.containsKey(modelField.getName())) {
347+
result.put(fieldName, serializedData.get(modelField.getName()));
348+
}
349+
} else {
350+
result.put(fieldName, extractFieldValue(modelField, instance));
351+
}
345352
} else if (association.isOwner()) {
346353
result.put(association.getTargetName(), extractAssociateId(modelField, instance));
347354
}

aws-datastore/src/main/java/com/amplifyframework/datastore/storage/StorageItemChange.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.amplifyframework.core.model.Model;
2222
import com.amplifyframework.core.model.ModelSchema;
2323
import com.amplifyframework.core.model.query.predicate.QueryPredicate;
24+
import com.amplifyframework.datastore.appsync.SerializedModel;
2425

2526
import java.util.Objects;
2627
import java.util.UUID;
@@ -37,6 +38,7 @@ public final class StorageItemChange<T extends Model> {
3738
private final Type type;
3839
private final QueryPredicate predicate;
3940
private final T item;
41+
private final SerializedModel patchItem;
4042
private final ModelSchema modelSchema;
4143

4244
private StorageItemChange(
@@ -45,12 +47,14 @@ private StorageItemChange(
4547
Type type,
4648
QueryPredicate predicate,
4749
T item,
50+
SerializedModel patchItem,
4851
ModelSchema modelSchema) {
4952
this.changeId = changeId;
5053
this.initiator = initiator;
5154
this.type = type;
5255
this.predicate = predicate;
5356
this.item = item;
57+
this.patchItem = patchItem;
5458
this.modelSchema = modelSchema;
5559
}
5660

@@ -101,6 +105,15 @@ public T item() {
101105
return item;
102106
}
103107

108+
/**
109+
* Gets a SerializedModel containing only the fields that have changed.
110+
* @return a SerializedModel containing only the fields that have changed.
111+
*/
112+
@NonNull
113+
public SerializedModel patchItem() {
114+
return patchItem;
115+
}
116+
104117
/**
105118
* Gets the schema of the changed item.
106119
* @return Schema of changed item
@@ -183,6 +196,7 @@ public static final class Builder<T extends Model> {
183196
private Type type;
184197
private QueryPredicate predicate;
185198
private T item;
199+
private SerializedModel patchItem;
186200
private ModelSchema modelSchema;
187201

188202
/**
@@ -256,6 +270,16 @@ public Builder<T> item(@NonNull T item) {
256270
return this;
257271
}
258272

273+
/**
274+
* Configures the patchItem, a SerializedModel containing only the fields that changed.
275+
* @param patchItem Representation of the changes that occurred.
276+
* @return Current Builder instance for fluent configuration chaining.
277+
*/
278+
public Builder<T> patchItem(@NonNull SerializedModel patchItem) {
279+
this.patchItem = Objects.requireNonNull(patchItem);
280+
return this;
281+
}
282+
259283
/**
260284
* Configures the schema of the item that changed.
261285
* @param modelSchema Schema of the item that changed
@@ -281,6 +305,7 @@ public StorageItemChange<T> build() {
281305
Objects.requireNonNull(type),
282306
Objects.requireNonNull(predicate),
283307
Objects.requireNonNull(item),
308+
Objects.requireNonNull(patchItem),
284309
Objects.requireNonNull(modelSchema)
285310
);
286311
}

0 commit comments

Comments
 (0)