Skip to content

Commit ddda926

Browse files
authored
Better POJO Support for Document Fields
* Allow passing POJOs as field values throughout API. * As values in a Map<> passed to set(). * As values to a update() call. * As elements in an array transform operation. * As values in query field filters. * Removed set(Map<>) overloads since they are now redundant with set(Object). * Added `T get(..., Class<T>, ...)` overloads for extracting fields as a custom class (POJO) type.
1 parent 24ae317 commit ddda926

File tree

12 files changed

+215
-182
lines changed

12 files changed

+215
-182
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# Unreleased
2+
- [feature] Custom objects (POJOs) can now be passed as a field value in
3+
update(), within `Map<>` objects passed to set(), in array transform
4+
operations, and in query filters.
5+
- [feature] DocumentSnapshot.get() now supports retrieving fields as
6+
custom objects (POJOs) by passing a Class<T> instance, e.g.
7+
`snapshot.get("field", CustomType.class)`.
28

39
# 17.1.2
410
- [changed] Changed the internal handling for locally updated documents that

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/POJOTest.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@
1515
package com.google.firebase.firestore;
1616

1717
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection;
18+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testDocument;
1819
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
1920
import static com.google.firebase.firestore.testutil.TestUtil.expectError;
21+
import static com.google.firebase.firestore.testutil.TestUtil.map;
2022
import static junit.framework.Assert.assertEquals;
2123

2224
import android.support.test.runner.AndroidJUnit4;
25+
import com.google.android.gms.tasks.Task;
26+
import com.google.android.gms.tasks.Tasks;
2327
import com.google.firebase.Timestamp;
2428
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
29+
import java.util.ArrayList;
2530
import java.util.Date;
2631
import org.junit.After;
2732
import org.junit.Test;
@@ -175,7 +180,7 @@ public void testWriteAndRead() {
175180
}
176181

177182
@Test
178-
public void testUpdate() {
183+
public void testSetMerge() {
179184
CollectionReference collection = testCollection();
180185
POJO data = new POJO(1.0, "a", collection.document());
181186
DocumentReference reference = waitFor(collection.add(data));
@@ -190,6 +195,55 @@ public void testUpdate() {
190195
assertEquals(expected, doc.toObject(POJO.class));
191196
}
192197

198+
// General smoke test that makes sure APIs accept POJOs.
199+
@Test
200+
public void testAPIsAcceptPOJOsForFields() {
201+
DocumentReference ref = testDocument();
202+
ArrayList<Task<?>> tasks = new ArrayList<>();
203+
204+
// as Map<> entries in a set() call.
205+
POJO data = new POJO(1.0, "a", ref);
206+
tasks.add(ref.set(map("a", data, "b", map("c", data))));
207+
208+
// as Map<> entries in an update() call.
209+
tasks.add(ref.update(map("a", data)));
210+
211+
// as field values in an update() call.
212+
tasks.add(ref.update("c", data));
213+
214+
// as values in arrayUnion() / arrayRemove().
215+
tasks.add(ref.update("c", FieldValue.arrayUnion(data)));
216+
tasks.add(ref.update("c", FieldValue.arrayRemove(data)));
217+
218+
// as Query parameters.
219+
data.setBlob(null); // blobs are broken, see b/117680212
220+
tasks.add(testCollection().whereEqualTo("field", data).get());
221+
222+
waitFor(Tasks.whenAll(tasks));
223+
}
224+
225+
@Test
226+
public void testDocumentSnapshotGetWithPOJOs() {
227+
DocumentReference ref = testDocument();
228+
229+
// Go offline so that we can verify server timestamp behavior overload.
230+
ref.getFirestore().disableNetwork();
231+
232+
POJO pojo = new POJO(1.0, "a", ref);
233+
ref.set(map("field", pojo));
234+
235+
DocumentSnapshot snap = waitFor(ref.get());
236+
237+
assertEquals(pojo, snap.get("field", POJO.class));
238+
assertEquals(pojo, snap.get(FieldPath.of("field"), POJO.class));
239+
assertEquals(
240+
pojo, snap.get("field", POJO.class, DocumentSnapshot.ServerTimestampBehavior.DEFAULT));
241+
assertEquals(
242+
pojo,
243+
snap.get(
244+
FieldPath.of("field"), POJO.class, DocumentSnapshot.ServerTimestampBehavior.DEFAULT));
245+
}
246+
193247
@Test
194248
public void setFieldMaskMustHaveCorrespondingValue() {
195249
CollectionReference collection = testCollection();

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ServerTimestampTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,28 @@ public void testServerTimestampsUsesPreviousValueFromLocalMutation() {
248248
assertThat(remoteSnapshot.get("a")).isInstanceOf(Timestamp.class);
249249
}
250250

251+
@Test
252+
public void testServerTimestampBehaviorOverloadsOfDocumentSnapshotGet() {
253+
writeInitialData();
254+
waitFor(docRef.update(updateData));
255+
DocumentSnapshot snap = accumulator.awaitLocalEvent();
256+
257+
// Default behavior should return null timestamp (via any overload).
258+
assertNull(snap.get("when"));
259+
assertNull(snap.get(FieldPath.of("when")));
260+
assertNull(snap.get("when", Timestamp.class));
261+
assertNull(snap.get(FieldPath.of("when"), Timestamp.class));
262+
263+
// Estimate should return a Timestamp object (via any overload).
264+
assertThat(snap.get("when", ServerTimestampBehavior.ESTIMATE)).isInstanceOf(Timestamp.class);
265+
assertThat(snap.get(FieldPath.of("when"), ServerTimestampBehavior.ESTIMATE))
266+
.isInstanceOf(Timestamp.class);
267+
assertThat(snap.get("when", Timestamp.class, ServerTimestampBehavior.ESTIMATE))
268+
.isInstanceOf(Timestamp.class);
269+
assertThat(snap.get(FieldPath.of("when"), Timestamp.class, ServerTimestampBehavior.ESTIMATE))
270+
.isInstanceOf(Timestamp.class);
271+
}
272+
251273
@Test
252274
public void testServerTimestampsWorkViaTransactionSet() {
253275
waitFor(

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -257,22 +257,16 @@ public void writesMustNotContainReservedFieldNames() {
257257

258258
@Test
259259
public void setsMustNotContainFieldValueDelete() {
260-
// PORTING NOTE: We avoid using expectSetError(), since it hits the POJO overload which
261-
// can't handle FieldValue.delete().
262-
DocumentReference ref = testDocument();
263-
expectError(
264-
() -> ref.set(map("foo", FieldValue.delete())),
260+
expectSetError(
261+
map("foo", FieldValue.delete()),
265262
"Invalid data. FieldValue.delete() can only be used with update() and set() with "
266263
+ "SetOptions.merge() (found in field foo)");
267264
}
268265

269266
@Test
270267
public void updatesMustNotContainNestedFieldValueDeletes() {
271-
// PORTING NOTE: We avoid using expectSetError(), since it hits the POJO overload which
272-
// can't handle FieldValue.delete().
273-
DocumentReference ref = testDocument();
274-
expectError(
275-
() -> ref.update(map("foo", map("bar", FieldValue.delete()))),
268+
expectUpdateError(
269+
map("foo", map("bar", FieldValue.delete())),
276270
"Invalid data. FieldValue.delete() can only appear at the top level of your update data "
277271
+ "(found in field foo.bar)");
278272
}
@@ -376,8 +370,8 @@ public void arrayTransformsFailInQueries() {
376370
@Test
377371
public void arrayTransformsRejectInvalidElements() {
378372
DocumentReference doc = testDocument();
379-
String reason = "Invalid data. Unsupported type: com.google.firebase.firestore.ValidationTest";
380-
// TODO: If we get more permissive with POJOs, perhaps we should make this work.
373+
String reason =
374+
"No properties to serialize found on class com.google.firebase.firestore.ValidationTest";
381375
expectError(() -> doc.set(map("x", FieldValue.arrayUnion(1, this))), reason);
382376
expectError(() -> doc.set(map("x", FieldValue.arrayRemove(1, this))), reason);
383377
}
@@ -558,15 +552,8 @@ private static void expectWriteError(
558552
DocumentReference ref = testDocument();
559553

560554
if (includeSets) {
561-
if (data instanceof Map) {
562-
@SuppressWarnings("unchecked")
563-
Map<String, Object> setMap = (Map<String, Object>) data;
564-
expectError(() -> ref.set(setMap), reason);
565-
expectError(() -> ref.getFirestore().batch().set(ref, setMap), reason);
566-
} else {
567-
expectError(() -> ref.set(data), reason);
568-
expectError(() -> ref.getFirestore().batch().set(ref, data), reason);
569-
}
555+
expectError(() -> ref.set(data), reason);
556+
expectError(() -> ref.getFirestore().batch().set(ref, data), reason);
570557
}
571558

572559
if (includeUpdates) {
@@ -593,13 +580,7 @@ private static void expectWriteError(
593580
(Function<Void>)
594581
transaction -> {
595582
if (includeSets) {
596-
if (data instanceof Map) {
597-
@SuppressWarnings("unchecked")
598-
Map<String, Object> setMap = (Map<String, Object>) data;
599-
expectError(() -> transaction.set(ref, setMap), reason);
600-
} else {
601-
expectError(() -> transaction.set(ref, data), reason);
602-
}
583+
expectError(() -> transaction.set(ref, data), reason);
603584
}
604585
if (includeUpdates) {
605586
assertTrue("update() only support Maps.", data instanceof Map);

firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.google.firebase.firestore.model.ResourcePath;
2424
import com.google.firebase.firestore.util.Executors;
2525
import com.google.firebase.firestore.util.Util;
26-
import java.util.Map;
2726
import javax.annotation.Nullable;
2827

2928
/**
@@ -117,12 +116,13 @@ public DocumentReference document(@NonNull String documentPath) {
117116
* Adds a new document to this collection with the specified data, assigning it a document ID
118117
* automatically.
119118
*
120-
* @param data A Map containing the data for the new document.
119+
* @param data The data to write to the document (e.g. a Map or a POJO containing the desired
120+
* document contents).
121121
* @return A Task that will be resolved with the DocumentReference of the newly created document.
122122
*/
123123
@NonNull
124124
@PublicApi
125-
public Task<DocumentReference> add(@NonNull Map<String, Object> data) {
125+
public Task<DocumentReference> add(@NonNull Object data) {
126126
checkNotNull(data, "Provided data must not be null.");
127127
final DocumentReference ref = document();
128128
return ref.set(data)
@@ -134,17 +134,4 @@ public Task<DocumentReference> add(@NonNull Map<String, Object> data) {
134134
return ref;
135135
});
136136
}
137-
138-
/**
139-
* Adds a new document to this collection with the specified POJO as contents, assigning it a
140-
* document ID automatically.
141-
*
142-
* @param pojo The POJO that will be used to populate the contents of the document
143-
* @return A Task that will be resolved with the DocumentReference of the newly created document.
144-
*/
145-
@NonNull
146-
@PublicApi
147-
public Task<DocumentReference> add(@NonNull Object pojo) {
148-
return add(firestore.getDataConverter().convertPOJO(pojo));
149-
}
150137
}

firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,13 @@ public CollectionReference collection(@NonNull String collectionPath) {
143143
* Overwrites the document referred to by this DocumentReference. If the document does not yet
144144
* exist, it will be created. If a document already exists, it will be overwritten.
145145
*
146-
* @param data A map of the fields and values for the document.
146+
* @param data The data to write to the document (e.g. a Map or a POJO containing the desired
147+
* document contents).
147148
* @return A Task that will be resolved when the write finishes.
148149
*/
149150
@NonNull
150151
@PublicApi
151-
public Task<Void> set(@NonNull Map<String, Object> data) {
152+
public Task<Void> set(@NonNull Object data) {
152153
return set(data, SetOptions.OVERWRITE);
153154
}
154155

@@ -157,13 +158,14 @@ public Task<Void> set(@NonNull Map<String, Object> data) {
157158
* exist, it will be created. If you pass {@link SetOptions}, the provided data can be merged into
158159
* an existing document.
159160
*
160-
* @param data A map of the fields and values for the document.
161+
* @param data The data to write to the document (e.g. a Map or a POJO containing the desired
162+
* document contents).
161163
* @param options An object to configure the set behavior.
162164
* @return A Task that will be resolved when the write finishes.
163165
*/
164166
@NonNull
165167
@PublicApi
166-
public Task<Void> set(@NonNull Map<String, Object> data, @NonNull SetOptions options) {
168+
public Task<Void> set(@NonNull Object data, @NonNull SetOptions options) {
167169
checkNotNull(data, "Provided data must not be null.");
168170
checkNotNull(options, "Provided options must not be null.");
169171
ParsedSetData parsed =
@@ -176,34 +178,6 @@ public Task<Void> set(@NonNull Map<String, Object> data, @NonNull SetOptions opt
176178
.continueWith(Executors.DIRECT_EXECUTOR, voidErrorTransformer());
177179
}
178180

179-
/**
180-
* Overwrites the document referred to by this DocumentReference. If the document does not yet
181-
* exist, it will be created. If a document already exists, it will be overwritten.
182-
*
183-
* @param pojo The POJO that will be used to populate the document contents
184-
* @return A Task that will be resolved when the write finishes.
185-
*/
186-
@NonNull
187-
@PublicApi
188-
public Task<Void> set(@NonNull Object pojo) {
189-
return set(firestore.getDataConverter().convertPOJO(pojo), SetOptions.OVERWRITE);
190-
}
191-
192-
/**
193-
* Writes to the document referred to by this DocumentReference. If the document does not yet
194-
* exist, it will be created. If you pass {@link SetOptions}, the provided data can be merged into
195-
* an existing document.
196-
*
197-
* @param pojo The POJO that will be used to populate the document contents
198-
* @param options An object to configure the set behavior.
199-
* @return A Task that will be resolved when the write finishes.
200-
*/
201-
@NonNull
202-
@PublicApi
203-
public Task<Void> set(@NonNull Object pojo, @NonNull SetOptions options) {
204-
return set(firestore.getDataConverter().convertPOJO(pojo), options);
205-
}
206-
207181
/**
208182
* Updates fields in the document referred to by this DocumentReference. If no document exists
209183
* yet, the update will fail.

firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,69 @@ public Object get(
289289
firestore.getFirestoreSettings().areTimestampsInSnapshotsEnabled()));
290290
}
291291

292+
/**
293+
* Returns the value at the field, converted to a POJO, or null if the field or document doesn't
294+
* exist.
295+
*
296+
* @param field The path to the field
297+
* @param valueType The Java class to convert the field value to.
298+
* @return The value at the given field or null.
299+
*/
300+
@Nullable
301+
public <T> T get(@NonNull String field, @NonNull Class<T> valueType) {
302+
return get(FieldPath.fromDotSeparatedPath(field), valueType, ServerTimestampBehavior.DEFAULT);
303+
}
304+
305+
/**
306+
* Returns the value at the field, converted to a POJO, or null if the field or document doesn't
307+
* exist.
308+
*
309+
* @param field The path to the field
310+
* @param valueType The Java class to convert the field value to.
311+
* @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet
312+
* been set to their final value.
313+
* @return The value at the given field or null.
314+
*/
315+
@Nullable
316+
public <T> T get(
317+
@NonNull String field,
318+
@NonNull Class<T> valueType,
319+
@NonNull ServerTimestampBehavior serverTimestampBehavior) {
320+
return get(FieldPath.fromDotSeparatedPath(field), valueType, serverTimestampBehavior);
321+
}
322+
323+
/**
324+
* Returns the value at the field, converted to a POJO, or null if the field or document doesn't
325+
* exist.
326+
*
327+
* @param fieldPath The path to the field
328+
* @param valueType The Java class to convert the field value to.
329+
* @return The value at the given field or null.
330+
*/
331+
@Nullable
332+
public <T> T get(@NonNull FieldPath fieldPath, @NonNull Class<T> valueType) {
333+
return get(fieldPath, valueType, ServerTimestampBehavior.DEFAULT);
334+
}
335+
336+
/**
337+
* Returns the value at the field, converted to a POJO, or null if the field or document doesn't
338+
* exist.
339+
*
340+
* @param fieldPath The path to the field
341+
* @param valueType The Java class to convert the field value to.
342+
* @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet
343+
* been set to their final value.
344+
* @return The value at the given field or null.
345+
*/
346+
@Nullable
347+
public <T> T get(
348+
@NonNull FieldPath fieldPath,
349+
@NonNull Class<T> valueType,
350+
@NonNull ServerTimestampBehavior serverTimestampBehavior) {
351+
Object data = get(fieldPath, serverTimestampBehavior);
352+
return data == null ? null : CustomClassMapper.convertToCustomClass(data, valueType);
353+
}
354+
292355
/**
293356
* Returns the value of the field as a boolean. If the value is not a boolean this will throw a
294357
* runtime exception.

0 commit comments

Comments
 (0)