3

I am trying to perform FieldValue.arrayUnion() on set(), but it is throwing an Exception. I'm using set instead of update, because I want it to create the document if not exists.

Code (in my case its Batch Write, but I think the principle is same)

WriteBatch batch = db.batch();
MyModel myModel = new MyModel("rwTEPzn9vjyhZCZxFeq8", "Mangesh"); // sample data
batch.set(docRef, FieldValue.arrayUnion(myModel), SetOptions.mergeFields("items"));  // throws exception

Stack trace:

W/System.err: java.lang.NullPointerException: Attempt to invoke virtual method 'com.google.firestore.v1.Value$ValueTypeCase com.google.firestore.v1.Value.getValueTypeCase()' on a null object reference
W/System.err:     at com.google.firebase.firestore.UserDataReader.convertAndParseDocumentData(com.google.firebase:firebase-firestore@@21.4.2:233)
W/System.err:     at com.google.firebase.firestore.UserDataReader.parseMergeData(com.google.firebase:firebase-firestore@@21.4.2:87)
W/System.err:     at com.google.firebase.firestore.WriteBatch.set(com.google.firebase:firebase-firestore@@21.4.2:89)
W/System.err:     at com.abc.AbcActivity.addTrans(AbcActivity.java:604)
W/System.err:     at com.abc.AbcActivity.validateInputs(AbcActivity.java:571)
W/System.err:     at com.abc.AbcActivity.onAddRecord(AbcActivity.java:260)
W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
W/System.err:     at android.view.View$DeclaredOnClickListener.onClick(View.java:5989)
W/System.err:     at android.view.View.performClick(View.java:7140)
W/System.err:     at android.view.View.performClickInternal(View.java:7117)
W/System.err:     at android.view.View.access$3500(View.java:801)
W/System.err:     at android.view.View$PerformClick.run(View.java:27351)
W/System.err:     at android.os.Handler.handleCallback(Handler.java:883)
W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:100)
W/System.err:     at android.os.Looper.loop(Looper.java:214)
W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:7356)
W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
W/System.err:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

UPDATE 1: MyModel is a very simple POJO class with couple of strings.

public class MyModel {
    private String id, name;

    public MyModel (String id, String name) {
        this.id = id;
        this.name = name;
    }

    @PropertyName("id")
    public String getId() {
        return id;
    }

    @PropertyName("id")
    public void setId(String id) {
        this.id = id;
    }

    @PropertyName("name")
    public String getName() {
        return name;
    }

    @PropertyName("name")
    public void setName(String name) {
        this.name= name;
    }
}

UPDATE 2: The exception is thrown for String and int values as well. That is, neither FieldValue.arrayUnion("abc") nor FieldValue.arrayUnion(1) works. I have also observed the same behaviour in case of arrayRemove().

UPDATE 3: Here is a screenshot of my db.

DB

UPDATE 4: I tested set outside of batch, directly on the document. It fails. Note that items was emptied before doing this to avoid any type-related conflicts.

docRef.set(FieldValue.arrayUnion("abc"), SetOptions.mergeFields("items"));

Looks like it is impossible to use arrayUnion and arrayRemove with set, despite of the documentation that says: "returns a special value that can be used with set() or update()"

Mangesh
  • 5,491
  • 5
  • 48
  • 71

2 Answers2

3

Finally, I found a solution. Here is the code:

Map<String, Object> map = new HashMap<>();
map.put("items", FieldValue.arrayUnion(myModel));
batch.set(docRef, map, SetOptions.merge());

This creates the document if not exists and updates items array. Note that, SetOptions.merge() is used instead of SetOptions.mergeFields().

Mangesh
  • 5,491
  • 5
  • 48
  • 71
  • I still wonder why `SetOptions.mergeFields()` didn't work, how to use it, in which scenario `SetOptions.mergeFields()` is appropriate. – Mangesh Jun 03 '20 at 13:37
  • Where is the new value `abc` that you want to be updated in the array? Check my updated answer. – Alex Mamo Jun 03 '20 at 13:40
  • You can use any value instead of `myModel`, doesn't matter if it is a string, a number or any custom object. – Mangesh Jun 03 '20 at 13:47
0

None of those method calls will work because your items array contains MyModel objects and not strings or numbers. The simplest method I can think of, it would be to get that array as a list of MyModel objects (List<MyModel>) and remove/add elements according to your needs.

Please also note, that in order to remove an entire object from the array, you should pass to that method, the entire object, with all fields not only partial data.

Edit:

How arrayUnion will get to know which array to update?

It will know because we need to pass as the first argument the name of your array property. So please use the following lines of code:

FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
WriteBatch batch = rootRef.batch();
DocumentReference docRef = rootRef.collection("yourColl").document("docId");
batch.update(docRef, "items", FieldValue.arrayUnion("abc"));
batch.commit();

The result will be the updated of your items array with a new element which is abc.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • It doesn't work with strings and numbers too. Check update 2. The real question here would be, as titled, "How to use arrayUnion in set operation?". – Mangesh Jun 03 '20 at 11:44
  • If you would have stored in the `items` array string elements and **not** `MyModel` objects, then FieldValue.arrayUnion("abc") would have been worked. If you would have stored in the `items` integers, FieldValue.arrayUnion(1) would have been worked too. There is no way it can work using actual schema unless you pass a full object as argument. More info **[here](https://stackoverflow.com/questions/61017457/android-firestore-querying-particular-value-in-array-of-objects/61027976#61027976)**. – Alex Mamo Jun 03 '20 at 12:06
  • Nope. I removed all elements from `items` and tried `batch.set(docRef, FieldValue.arrayUnion("abc"), SetOptions.mergeFields("items"));`. It fails. – Mangesh Jun 03 '20 at 12:22
  • There is no need to add that to a batch, that operation is already atomic. Try to add it directly, exactly as the [docs](https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array) stay. – Alex Mamo Jun 03 '20 at 12:24
  • I'm combining this `set` with other write operations hence the batch. – Mangesh Jun 03 '20 at 12:38
  • I understand that but you cannot update an array that way. `FieldValue.arrayUnion("abc")` already does what `SetOptions.mergeFields("items"))` is supposed to do. So it should be removed. Should be something like this `batch.update(docRef, {FieldValue.arrayUnion("abc")});` Does it work this way? – Alex Mamo Jun 03 '20 at 12:58
  • Nope. That produces syntax error. Please check update 4. – Mangesh Jun 03 '20 at 13:13
  • It does **not** make any sense to use `SetOptions.mergeFields("items")`. `FieldValue.arrayUnion("abc")` already does that. It will update your array with the desired value. However, if an element already exists, it will be overwritten. If you need duplicates, read the document, add the duplicate elements, write the document back. – Alex Mamo Jun 03 '20 at 13:21
  • How `arrayUnion` will get to know which array to update? Can you edit your answer and give me the correct syntax? – Mangesh Jun 03 '20 at 13:23
  • Please check my updated answer with the full syntax. Tell me, does it work now? – Alex Mamo Jun 03 '20 at 13:37
  • Actually no. You are using `update` and I want to use `set`, which gives me an advantage of creating the document if not exists. Check my answer and please read my comment below it. Thanks! – Mangesh Jun 03 '20 at 13:43
  • @Mangesh Please leave it as a comment. – Alex Mamo Jun 04 '20 at 18:16