【问题标题】:Add new field or change the structure on all Firestore documents添加新字段或更改所有 Firestore 文档的结构
【发布时间】:2018-08-20 12:16:05
【问题描述】:

考虑users 的集合。集合中的每个文档都有 nameemail 作为字段。

{
  "users": {
    "uid1": {
      "name": "Alex Saveau",
      "email": "saveau.alexandre@gmail.com"
    },
    "uid2": { ... },
    "uid3": { ... }
  }
}

现在考虑一下,使用这个有效的 Cloud Firestore 数据库结构,我启动了我的第一个移动应用程序版本。然后,在某个时候,我意识到我想包含另一个字段,例如 last_login

在代码中,使用 Java 从 Firestore DB 中读取所有用户文档的方式如下:

FirebaseFirestore.getInstance().collection("users").get()
        .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            @Override
            public void onComplete(@NonNull Task<QuerySnapshot> task) {
                if (task.isSuccessful()) {
                    for (DocumentSnapshot document : task.getResult()) {
                        mUsers.add(document.toObject(User.class));
                    }
                }
            }
        });

User 类现在包含nameemaillast_login

由于新的 User 字段 (last_login) 不包含在存储在数据库中的旧用户中,因此应用程序崩溃了,因为新的 User 类需要一个 last_login 字段,该字段返回为 @ 987654335@ 通过get() 方法。

last_login 包含在数据库的所有现有User 文档中而不丢失新版本应用程序的数据的最佳做法是什么?我应该只运行一次 sn-p 来完成这项任务还是有更好的方法来解决这个问题?

【问题讨论】:

  • Firebase 客户端通常不会因 JSON 中缺少的属性而崩溃。您能否分享重现崩溃所需的 minimal User 类,以及崩溃的完整堆栈跟踪?
  • 崩溃实际上并不是由 Firebase 方法引起的,而是因为我得到了一个null 值。到目前为止,我已经创建了一个函数来重命名集合中所有文档的键(字段)名称。我稍后会发布。
  • 我已经添加了一个答案。它包含我为解决此问题而编写的函数。它并不优雅,但很有效。
  • 我有同样的问题,但我希望解决方案应该从服务器端而不是客户端处理我认为最好的方法是在发布之前在服务器端执行脚本下一个版本,如果你想防止超出配额,我想最好使用分页应用脚本(n)天,何时完成发布下一个更新等等

标签: java android database google-cloud-firestore


【解决方案1】:

您陷入了 NOSQL 数据库的空白:面向文档的数据库不保证数据的结构完整性(正如 RDBMS 所做的那样)

交易是:

  • RDBMS中,所有存储的数据在任何给定时间(在同一实例或集群内)都具有相同的结构。更改结构(ER 图)时,您必须迁移所有现有记录的数据,这会耗费时间和精力。

    因此,您的应用程序可以针对当前版本的数据结构进行优化。

  • 面向文档的数据库中,每条记录都是一个具有自己独立结构的独立“页面”。如果您更改结构,它仅适用于 new 文档。因此您无需迁移现有数据。

    因此,您的应用程序必须能够处理您曾经在当前数据库中使用过的所有版本的数据结构。

我不详细了解 firebase,但一般来说,您从不更新 NOSQL 数据库中的文档。您只需创建文档的新版本。因此,即使您更新了所有文档,您的应用程序也必须准备好处理“旧”数据结构...

【讨论】:

  • 感谢您提供的信息,这很有用。我编写了一个小函数来更新集合中所有文档的字段名称。这是以一种艰难的方式完成的(删除旧字段,编写一个新字段),我期待与 Firestore 更集成的东西。到现在都没找到。
  • 看到这个stackoverflow.com/questions/52109026/… 但试图看看我们是否可以做类似protobufs的事情。我们可以将版本信息从客户端发送到 Firestore 或其他东西,这样 Firestore 就可以在决定使用哪个版本方面做一些智能的事情?
  • @Timothy Truckle:“您的应用程序必须准备好处理“旧”数据结构”——您对如何处理 NOSQL 数据库有任何建议/参考吗?由此看来,每个文档都应该带有一个应用程序检查的版本号,但这似乎太过分了......谢谢
  • @smörkex 是的,由您的应用程序维护的结构版本号是我认为有效解决此问题的唯一方法。另一种方法是“尝试”结构版本,直到找到匹配的版本。无论如何,这听起来不像击球手......
  • @b-fg 请问你是怎么做的?代码??还是可以直接在firebase控制台中完成。?或者您能否提供用于向集合中的每个文档添加字段的代码。 Tnks
【解决方案2】:

当我发布问题时,我写了一些例程来帮助自动化这个过程。我没有发布它们,因为它们有点初级,我希望有一个优雅的基于 Firestore 的解决方案。因为这样的解决方案还没有,这里是我写的函数。

简而言之,我们有重命名字段、添加字段或删除字段的功能。要重命名字段,根据数据类型使用不同的函数。也许有人可以更好地概括这一点?下面的函数是:

  • add_field:在集合的所有文档中添加一个字段。
  • delete_field:删除集合的所有文档中的字段。
  • rename_*_field:重命名集合的所有文档中包含特定数据类型 (*) 的字段。这里我包含了字符串、整数和日期的示例。

添加字段

public void add_field (final String key, final Object value, final String collection_ref) {
    FirebaseFirestore.getInstance().collection(collection_ref).get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {
                        WriteBatch batch = db.batch();

                        for (DocumentSnapshot document : task.getResult()) {
                            DocumentReference docRef = document.getReference();
                            Map<String, Object> new_map = new HashMap<>();
                            new_map.put(key, value);
                            batch.update(docRef, new_map);
                        }
                        batch.commit();
                    } else {
                        // ... "Error adding field -> " + task.getException()
                    }
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    // ... "Failure getting documents -> " + e
                }
            });
}

删除字段

public void delete_field (final String key, final String collection_ref) {
    FirebaseFirestore.getInstance().collection(collection_ref).get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {

                        WriteBatch batch = db.batch();

                        for (DocumentSnapshot document : task.getResult()) {
                            DocumentReference docRef = document.getReference();
                            Map<String, Object> delete_field = new HashMap<>();
                            delete_field.put(key, FieldValue.delete());
                            batch.update(docRef, delete_field);
                        }
                        // Commit the batch
                        batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
                            @Override
                            public void onComplete(@NonNull Task<Void> task) {
                                // ...
                            }
                        });

                    } else {
                        // ... "Error updating field -> " + task.getException()
                    }
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    // ... "Failure getting notices -> " + e
                }
            });
}

重命名字段

public void rename_string_field (final String old_key, final String new_key, final String collection_ref) {
    FirebaseFirestore.getInstance().collection(collection_ref).get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {

                        WriteBatch batch = db.batch();

                        for (DocumentSnapshot document : task.getResult()) {
                            DocumentReference docRef = document.getReference();
                            String old_value = document.getString(old_key);

                            if (old_value != null) {
                                Map<String, Object> new_map = new HashMap<>();
                                new_map.put(new_key, old_value);

                                Map<String, Object> delete_old = new HashMap<>();
                                delete_old.put(old_key, FieldValue.delete());

                                batch.update(docRef, new_map);
                                batch.update(docRef, delete_old);
                            }
                        }
                        // Commit the batch
                        batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
                            @Override
                            public void onComplete(@NonNull Task<Void> task) {
                                // ...
                            }
                        });

                    } else {
                        // ... "Error updating field -> " + task.getException()
                    }
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    // ... "Failure getting notices ->" + e
                }
            });
}

public void rename_integer_field (final String old_key, final String new_key, final String collection_ref) {
    FirebaseFirestore.getInstance().collection(collection_ref).get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {

                        WriteBatch batch = db.batch();

                        for (DocumentSnapshot document : task.getResult()) {
                            DocumentReference docRef = document.getReference();
                            int old_value = document.getDouble(old_key).intValue();
                            Integer ov = old_value;
                            if (ov != null) {
                                Map<String, Object> new_map = new HashMap<>();
                                new_map.put(new_key, old_value);

                                Map<String, Object> delete_old = new HashMap<>();
                                delete_old.put(old_key, FieldValue.delete());

                                batch.update(docRef, new_map);
                                batch.update(docRef, delete_old);
                            }
                        }
                        // Commit the batch
                        batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
                            @Override
                            public void onComplete(@NonNull Task<Void> task) {
                                // ...
                            }
                        });

                    } else {
                        // ... "Error updating field -> " + task.getException()
                    }
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    // ... "Failure getting notices -> " + e
                }
            });
}

public void rename_date_field (final String old_key, final String new_key, final String collection_ref) {
    FirebaseFirestore.getInstance().collection(collection_ref).get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {

                        WriteBatch batch = db.batch();

                        for (DocumentSnapshot document : task.getResult()) {
                            DocumentReference docRef = document.getReference();
                            Date old_value = document.getDate(old_key);
                            if (old_value != null) {
                                Map<String, Object> new_map = new HashMap<>();
                                new_map.put(new_key, old_value);

                                Map<String, Object> delete_old = new HashMap<>();
                                delete_old.put(old_key, FieldValue.delete());

                                batch.update(docRef, new_map);
                                batch.update(docRef, delete_old);
                            }
                        }
                        // Commit the batch
                        batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
                            @Override
                            public void onComplete(@NonNull Task<Void> task) {
                                // ...
                            }
                        });

                    } else {
                        // ... "Error updating field -> " + task.getException()
                    }
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    // ... "Failure getting notices -> " + e
                }
            });
}

【讨论】:

    【解决方案3】:

    要解决这个问题,您需要更新每个用户以拥有新属性,为此我建议您使用Map。如果您在创建用户时使用模型类,如我对此 post 的回答中所述,要更新所有用户,只需遍历 users 集合并使用以下代码:

    Map<String, Object> map = new HashMap<>();
    map.put("timestamp", FieldValue.serverTimestamp());
    userDocumentReference.set(map, SetOptions.merge());
    

    【讨论】:

    • @KumarSantanu 不客气,Kumar。
    【解决方案4】:

    只是想分享一下,因为我读到您希望有一个基于 Firestore 的解决方案。

    这对我有用。 forEach 将查询集合中的每个文档,您可以随意操作。

        db.collection("collectionName").get().then(function(querySnapshot) {
            querySnapshot.forEach(async function(doc) {
                await db.collection("collectionName").doc(doc.id).set({newField: value}, {merge: true});
                // doc.data() is never undefined for query doc snapshots
                console.log(doc.id, " => ", doc.data());
            });
        });

    【讨论】:

    • 金牌.doc(doc.id).set({newField: value}, {merge: true});
    • 这个答案对我帮助很大。谢谢。
    【解决方案5】:

    我猜last_login 是一个原始数据类型,可能是一个long 来保存时间戳。自动生成的 setter 如下所示:

    private long last_login;
    
    public void setLast_login(long last_login) {
        this.last_login = last_login;
    }
    

    当由于对原始数据类型的变量进行空分配而获取缺少该字段的旧文档时,这会导致崩溃。 一种解决方法是修改您的 setter 以传入等效包装类的变量 - 在这种情况下,Long 而不是 long,并在 setter 中进行空检查。

    private long last_login;
    
    public void setLast_login(Long last_login) {
        if(last_login != null) {
            this.last_login = last_login;
        }
    }
    

    避免空指针异常的代价是装箱拆箱开销。

    【讨论】:

    • 其实很好的解决方案,+1
    猜你喜欢
    • 1970-01-01
    • 2019-07-15
    • 2022-11-25
    • 1970-01-01
    • 2014-05-29
    • 2022-01-03
    • 1970-01-01
    • 2018-12-24
    • 1970-01-01
    相关资源
    最近更新 更多