Consider a collection of users
. Each document in the collection has name
and email
as fields.
{
"users": {
"uid1": {
"name": "Alex Saveau",
"email": "saveau.alexandre@gmail.com"
},
"uid2": { ... },
"uid3": { ... }
}
}
Consider now that with this working Cloud Firestore database structure I launch my first version of a mobile application. Then, at some point I realize I want to include another field such as last_login
.
In the code, reading all the users documents from the Firestore DB using Java would be done as
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));
}
}
}
});
where the class User
contains now name
, email
and last_login
.
Since the new User
field (last_login
) is not included in the old users stored in the DB, the application is crashing because the new User
class is expecting a last_login
field which is returned as null
by the get()
method.
What would be the best practice to include last_login
in all the existing User
documents of the DB without losing their data on a new version of the app? Should I run an snippet just once to do this task or are there any better approaches to the problem?
I wrote some routines to help automate this process back when I posted the question. I did not post them since these are a bit rudimentary and I was hoping for an elegant Firestore-based solution. Because such solution is not still available, here are the functions I wrote.
In short, we have functions for renaming a field, adding a field, or deleting a field. To rename a field, different functions are used depending on the data type. Maybe someone could generalise this better?. The function you will find below are:
Add field:
Delete field:
Rename field:
You fell into a gap of NOSQL databases: Document oriented databases do not guarantee structural integrity of the data (as RDBMS do)
The deal is:
in an RDBMS all stored data have the same structure at any given time (within the same instance or cluster). When changing the structure (ER-diagram) you have to migrate the data for all existing records which costs time and efford.
In result you application can be optimised for the current version of the data structure.
in a Document oriented database each record is an independend "Page" with its own independend structure. If you change the struckture it only applies to new documents. So you don't need to migrate the exiting data.
In result you application must be able to deal with all versions of your data structure you ever used in your current database.
I don't know about firebase in detail but in general you never update a document in a NOSQL database. You only create a new version of the document. So even if you update all documents your application must be prepaed to deal with the "old" data structure...
To solve this, you need to update each user to have the new property and for that I recommend you to use a
Map
. If you are using a model class when you are creating the users as explained in my answer from this post, to update all users, just iterate over theusers
collection amd use the following code:I am guessing that
last_login
is a primitive data type, maybe along
to hold a timestamp. An auto-generated setter would look like this:This leads to a crash when old documents that lack the field are fetched due to null assignment to a variable of a primitive data type. One way around it is to modify your setter to pass in a variable of the equivalent wrapper class -
Long
instead oflong
in this case, and put a null check in the setter.The cost of avoiding the null pointer exception is the boxing-unboxing overhead.