I'm learning domain driven design for Flutter apps. I understand that the model is used between the infrastructure layer and the use-case, and the entity is used between in the use case and the UI.
Let's say that my app is dealing with books and I'm storing my books in Cloud Firestore. I have defined a very simple BookEntity with an id and a title.
#freezed
class BookEntity with _$BookEntity {
const factory BookEntity({
required String firestoreId, // This is the ID of the document in firestore
required String title,
}) = _BookEntity;
}
I believe that the ID of the document should be in the entity, because if I need to modify this book in the Firestore I will need to know the reference of the document, right?
As you know, in firestore the id is not part of the data themselves. When I read my database, I would be using the code below.
// code not tested
FirebaseFirestore
.instance
.collection('books')
.doc('uXSin0z3gqPHwhVLCP98')
.get()
.then((DocumentSnapshot<Map<String, dynamic>> snapshot) {
snapshot.id; // -> 'uXSin0z3gqPHwhVLCP98'
snapshot.data(); // -> {title: 'To Kill a Mockingbird', price: 11.69, year: 1960}
});
Somewhere, I will need to put the id together with the data. I think the right place to do it is in the model, which is created in the repository. I think my model would probably look very similar to the Entity:
#freezed
class BookModel with _$BookModel {
const factory BookModel({
required String firestoreId,
required String title,
}) = _BookModel;
}
And, when fetching data from Firestore I would create a model with:
BookModel(
firestoreId: snapshot.id,
title: snapshot.data()?['title'],
);
This can then be converted to a BookEntity which will be consumed by the UI.
The problem is that when I am reversing the flow, when I am creating a new book, the ID of the firestore document is not known in the presentation and domain layers. Therefore my BookEntity and BookModel must be updated so that the id is optional. The entity and the model now look like this
#freezed
class BookEntity with _$BookEntity {
const factory BookEntity({
String? firestoreId,
required String title,
}) = _BookEntity;
}
#freezed
class BookModel with _$BookModel {
const factory BookModel({
String? firestoreId,
required String title,
}) = _BookModel;
}
The problem is that now, every time I need to access the firestoreId field of my BookEntity, whose data originate from Firestore, I need to test whether the firestoreId field is null or not. But it cannot be null because the data come from Firestore, so there is always an ID. So I will either write a lot of null-checks, or use the ! (which I don't like).
In short, the "upstream" and "downstream" flows have different requirements for the firestoreId field. The Firebase -> UI flow needs a String, and the UI -> Firebase flow needs a String?.
So the question is what is the best and cleanest way to handle this?
firestoreId should be nullable, because it make sense for BookEntity to have firestoreId sometime and not have firestoreId sometime.
You probably don't need to use firestoreId in the UI, and It's only needed when writing to the Firestore. So you can have a writeToFirestore method and only use a single null check there.
You can also generate a new random id locally whenever a new BookEntity is created. Using your own document id when creating a document in Firestore
One more solution is to use late final String firestoreId, but it's skipping null check making debuging harder and doesn't work with Freezed.
Related
I have a model as below:
// will refer to this as ProfileV1
#freezed
class Profile with _$Profile {
// stands for anon users in firebase auth
const factory Profile.anonymous({
// stands for the id of anon users in firebase auth
required String id,
}) = _AnonymousProfile;
// stands for authed users in firebase auth
const factory Profile.authenticated({
required String id,
required String username,
}) = _AuthenticatedProfile;
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
}
And I work on ProfileV1 for a while, I save some documents with this schema to Firestore. However, a while later, I'd like to introduce a new field to Profile.authenticated variant as below:
// will refer to this as ProfileV2
#freezed
class Profile with _$Profile {
const factory Profile.anonymous({
required String id,
}) = _AnonymousProfile;
const factory Profile.authenticated({
required String id,
required String username,
// new field, simple for the sake of simplicity
required int upvotes,
}) = _AuthenticatedProfile;
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
}
Now, if I'd like to get the old ProfileV1 documents I've saved in Firestore, Profile.fromJson of ProfileV2 will probably throw an error saying "There's no upvotes field on the fetched document." of some sort.
How do you deal with this situation? How do you (preferably, lazily) migrate a data schema to another after you fetch it from Firestore?
Environment
Flutter 3.3.5
Dart 2.18.2
Freezed 2.2.0
I had a doubt like how to handle field does not exists situation like suppose i have released my app and in future updates i added a new field in doc then how can i handle if field does not exists.
For example, in shared preferences we use ??to handle data existence with the specified key value.
int val=prefs.getInt("myKey")??0;
as you can see that above code will set value of val to 0 if there's no value associated with the key- myKey. Similarly i would like to know is there any way of doing it for firestore document fields.
MyCode:-
class UserModel
{
final String? id;
final String? username;
final String? email;
UserModel({
this.id,
this.username,
this.email,
});
factory UserModel.fromDocument(DocumentSnapshot doc)
{
return UserModel(
id: doc['id'],
username: doc['username'],//suppose the username does not exist in the field then how can i assign the value "User" to the username?
email: doc['email'],
);
}
}
This is the problem when developing an app which use Firebase. If you add a field after there is already a data and if you try to get new field in dart, you are getting this error. I think the only solution is deleting all the old data which doesn't have new field. Or put this new field to all of your old data. It's kinda annoying. So make sure that you have to add all possible field in the beginning.
Unfortunately, you can't use like int val=prefs.getInt("myKey")??0; because it isn't even null.
I'm making an app using Flutter, with Cloud Firestore for the backend. I have a stream which retrieves a list of user documents for all users and want to filter the users whose favorite food is "pasta". I don't want to load the other documents. Here is my stream, and the function which maps it to my user model.
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('Users');``
List<MyAppUser> _userListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((DocumentSnapshot doc) {
return MyAppUser(
uid: doc.id ?? '',
name: (doc['name']).toString() ?? '',
email: (doc['email']).toString() ?? '',
favorite_food: (doc['favorite food']).toString() ?? '',
);
}).toList();
}
Stream<List<MyAppUser>> get users {
return usersCollection.snapshots().map(_userListFromSnapshot);
}
Here is my user model if needed:
class MyAppUser{
final String uid;
final String name;
final String email;
final String favorite_food;
MyAppUser({
this.name,
this.email,
this.uid,
this.favorite_food,
});
}
Should I use a where function after mapping or before?
If I filter before mapping, I will have to do a where on the original stream like
usersCollection.where('favorite food', isEqualTo: 'pasta')
If I filter after mapping, I can get type safety:
I listen to the stream with Provider: final users = Provider.of<List<MyAppUser>>(context);
Then query like this:
users.where((user) => user.favorite_food == 'pasta');
I would prefer to use typesafety, but, will I be billed for reading only the filtered documents or all documents?
I got this answer from Aurimas Deimantas, after commenting on their article on medium.com. Below, I have adapted their answer to suit this question.
Firestore bills you based on how many document reads you have.
It will be better to filter before mapping, with
usersCollection.where('favorite food', isEqualTo: 'pasta')
because this will only read the documents where favorite food is pasta.
If you filter after mapping, like this:
users.where((user) => user.favorite_food == 'pasta');
then all user documents will be read, and after that, filtered. So Firestore will bill you for all the document reads instead of only those whose favorite food is pasta.
This is why it saves money to filter on the userscollection directly, before mapping it to your model.
If you want to map the stream to your model, you can do it after the where filter, by adding the .map(...) function after the .where(...) function, and this will map (& read) only the documents that pass the where filter, saving money.
You can use where clause just after collection calling like
... Collection('Users').where(field, conditions)
With this, you don't have filter list using collection
I am creating a collection of judges and courthouses. Every judge will be assigned to one courthouse. I have set up my relation to be that courthouse has many judges
I am attempting to do this programmatically when the app loads. I have a function that is able to populate all the fields in judge except the relation to courthouse. My function uses the Strapi API like this
const judge = await strapi.query('judge').create({
name: data[i].name,
},
{
courthouse: data[i].courthouse_name // here is where I think the relation is created
}
)
I am passing in a string that has the name of courthouse, because I don't know the ID of the courthouse in the Courthouse collection.
My question is it possible to create a relation to another collection by anything other than an ID? How can I create a relation to a courthouse by its name?
I couldn't find a way around building a relationship between two models without the ID, so I created a custom solution using the Strapi lifecycle hooks
Essentially what I did I utilized the beforeCreate lifecycle hook to query and find the courthouse that matches the name like this:
// judges.js
async beforeCreate(result, data) {
const courthouse = await strapi.query('courthouse').find(
{courthouse_name:data.courthouse}
); // returns the courthouse that matches the name
result['courthouse'] = courthouse[0].id; // populates the relational field with the
// ID of the courthouse
}
The response object contained the courthouse's ID and I manipulated the data that is being sent to the create command like this:
const judge = await strapi.query('judge').create({
name: data[i].name,
courthouse: data[i].courthouse_name
})
The result is an object that looks like this:
{name: 'Garfield Lucas, courthouse: 7463987}
The documentation talks about listening to a record, but how do I listen to a single field inside that record? Assuming the record contains map.
Supposed I have a User class as follows:
class User {
final String uid; // used as key in Sembast
final String name;
bool isVerified;
/// Used to store data to Sembast
Map<String, dynamic> toMap() => {
'uid': uid,
'name': name,
'isVerified': isVerified,
};
}
When I save it locally like this:
final _store = stringMapStoreFactory.store('userStore');
// Store data:
await _store.record(user.uid).put(database, user.toMap());
Then somewhere in the page I want to listen to changes to isVerified field without fetching the whole User information. How do I do that?
Because _store.record(user.uid).onSnapshot(database) returns Stream<RecordSnapshot> of the whole data of that User class.
Thanks
Similarly to firestore, you can only listen to a whole record change (or a query in a store) but not a single field change in a record. As a side note, fetching a "whole" record does not cost much, the record being already ready to use in memory.