Firestore replicating a SQL Join for noSQL and Flutter - firebase

I realise there is many questions in regards to replicating joins with NoSql document databases such as FireStore, however i'm unable to find a thorough solution utilising Dart/Flutter with FireStore.
I have done some research i feel that in the following example i would be looking for a 'many to many' relationship (please correct me if this is wrong) as there may be a future need to look at all profiles as well as all connections.
In firebase, i have two root level collections (profile & connection):
profile
> documentKey(Auto Generated)
> name = "John Smith"
> uid = "xyc4567"
> documentKey(Auto Generated)
> name = "Jane Doe"
> uid = "abc1234"
> documentKey(Auto Generated)
> name = "Kate Dee"
> uid = "efg8910"
connection
> documentKey(Auto Generated)
> type = "friend"
> profileuid = "abc1234"
> uid = "xyc4567"
> documentKey(Auto Generated)
> type = "family"
> profileuid = "abc1234"
> uid = "efg8910"
For this example the 'connection' documents have been created hypothetically for the user John Smith (uid: xyc4567) when he connected to Jane Doe (uid: abc1234) and Kate Dee (uid: efg8910).
Here is the relational SQL i'm looking to replicate to show a list of profiles which John Smith has connected with:
Select * FROM profile, connection
WHERE profile.uid = connection.profileuid
AND profile.uid = "xyc4567"
In flutter my flutter app i have a fireStore query starting point:
stream: Firestore.instance.collection('profile')
.where('uid', isEqualTo: "xyc4567").snapshots(),
Obviously it only returns from one collection. How do i join the collections in a many to many relationship?

Unfortunately, there is no JOIN clause in Cloud Firestore nor in others NoSQL databases. In Firestore queries are shallow. This means that they only get items from the collection that the query is run against. There is no way to get documents from two top-level collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use properties of documents in a single collection.
So the most simple solution I can think of is to query the database to get the uid of a user from the profile collection. Once you have that id, make another database call (inside the callback), and get the corresponding data that you need from the connection collection using the following query:
stream: Firestore.instance.collection('connection').where('uid', isEqualTo: "xyc4567").snapshots(),
Another solution would be to create a subcollection named connection under each user and add all connection objects beneath it. This practice is called denormalization and is a common practice when it comes to Firebase. If you are new to NoQSL databases, I recommend you see this video, Denormalization is normal with the Firebase Database for a better understanding. It is for Firebase realtime database but same rules apply to Cloud Firestore.
Also, when you are duplicating data, there is one thing that need to keep in mind. In the same way you are adding data, you need to maintain it. With other words, if you want to update/detele an item, you need to do it in every place that it exists.

Suppose, you want to use a Stream that depends on some Future objcets.
Stories
Document ID (Auto Generated) //Suppose, "zddgaXmdadfHs"
> name = "The Lion & the Warthog"
> coverImage = "https://...."
> author = "Furqan Uddin Fahad"
> publisDate = 123836249234
Favorites
Document ID (Auto Generated)
> storyDocID = "zddgaXmdadfHs" //Document ID of a story
> userId = "adZXkdfnhoa" //Document ID of a user
Sql equivalent query should look like this
SELECT * FROM Favorites AS f, Stories AS s
WHERE f.storyDocID = s.DocumentID
AND f.userId = user.userId
And Firestore query like this
final _storeInstance = Firestore.instance;
Stream <List<Favorite>> getFavorites() async* {
final user = await _getUser(); //_getUser() Returns Future<User>
yield* _storeInstance
.collection('Favorites')
.where('userId', isEqualTo: user.userId)
.snapshots()
.asyncMap((snapshot) async {
final list = snapshot.documents.map((doc) async {
final story = await _getStory(doc['storyDocID']);
return Favorite.from(doc, story); //Favorite.from(DocumentSnapshot doc, Story story) returns an instance of Favorite
}).toList(); //List<Future<Favorite>>
return await Future.wait(list); //Converts List<Future<Favorite>> to Future<List<Favorite>>
});
}
Future<Story> _getStory(String storyDocID) async {
final docRef = _storeInstance
.collection('Stories')
.document(storyDocID);
final document = await docRef.get();
final story = Story.from(document);
return story;
}

I did some like this to join results from two colections objects and categories.
i did two StreamBuilders to show in a list, in the first one i got the categories and put in a map, then i query the objects and get the category object from the map using the categoryID:
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('categoryPath')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> categorySnapshot) {
//get data from categories
if (!categorySnapshot.hasData) {
return const Text('Loading...');
}
//put all categories in a map
Map<String, Category> categories = Map();
categorySnapshot.data.documents.forEach((c) {
categories[c.documentID] =
Category.fromJson(c.documentID, c.data);
});
//then from objects
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('objectsPath')
.where('day', isGreaterThanOrEqualTo: _initialDate)
.where('day', isLessThanOrEqualTo: _finalDate)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> objectsSnapshot) {
if (!objectsSnapshot.hasData)
return const Text('Loading...');
final int count =
objectsSnapshot.data.documents.length;
return Expanded(
child: Container(
child: Card(
elevation: 3,
child: ListView.builder(
padding: EdgeInsets.only(top: 0),
itemCount: count,
itemBuilder: (_, int index) {
final DocumentSnapshot document =
objectsSnapshot.data.documents[index];
Object object = Object.fromJson(
document.documentID, document.data);
return Column(
children: <Widget>[
Card(
margin: EdgeInsets.only(
left: 0, right: 0, bottom: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(0)),
),
elevation: 1,
child: ListTile(
onTap: () {},
title: Text(object.description,
style: TextStyle(fontSize: 20)),
//here is the magic, i get the category name using the map
of the categories and the category id from the object
subtitle: Text(
categories[object.categoryId] !=
null
? categories[
object.categoryId]
.name
: 'Uncategorized',
style: TextStyle(
color: Theme.of(context)
.primaryColor),
),
),
),
],
);
}),
),
),
);
I'm not sure if is what you want or is clear but i hope it help you.

I think denominational should not be preferred because to maintain the it you have to make extra writes to firestore
instead jorge vieira is correct since you are allowed to make double reads as compare to the writes
so its better to read twice instead of writing writing data twice and its also very impractical to remember every demoralized thing in a large project

Related

Loading Firebase childs dont works

So I made some childs in Firebase and wanted to load them as a list view in Flutter, so I made a Scaffold and this is whats inside the Body:
FutureBuilder(
future: _Ref.get(),
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator.adaptive();
}
if(snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
List<Widget> _list = [];
_Ref.once().then((DataSnapshot) async {
final data = await DataSnapshot.snapshot.children.toList();
data.forEach((element) async {
final value = await element.value.toString();
print(value);
_list.add(ListTile(
title: Text(value),
trailing: Icon(
Icons.delete_outlined,
),
));
});
});
print(_list);
return ListView(
children: _list,
);
}
},
),
my output looks like this:
this is what my Firebase database looks like:
But the funny thing is that if I hotreload my app it works how it is supposed to
My educated guess is that you expected 2 lines in the output, instead of the 3 lines you get.
When the Firebase SDKs see sequential, numeric keys in a snapshot, they assume that this snapshot is an array. If there's one or a few keys missing in that array, it fill them with null values. So in your database screenshot, the Firebase SDK sees an array with indexes 1 and 2, and index 0 missing.
If you want to prevent this array coercion, don't use sequential numeric keys in your data structure. You can most easily prevent this by calling push() to generate a unique key for you, or you can prefix the existing values with a short non-numeric string, e.g. "key1", "key2".
I recommend checking out this vintage blog post: Best Practices: Arrays in Firebase on why using such array-like keys is typically an antipattern in Firebase.

Delete a document from firebase

I'm building an app that contains pet adoption offers. Each pet document has an ID that's generated by DateTime.now() + the user ID to make it unique, anyway, I'm trying to write a deleting method within the Slidable widget to delete the adoption offer.
The problem is that I'm unable to reach the document ID to delete it.
Is there a way to delete a document without getting the ID?
This is the Firebase database
Here is my current code
Future getOffersList() async {
List<PetTile> tiles = [];
List<Slidable> slidables = [];
var data = await FirebaseFirestore.instance
.collection('pets')
.where('owner',
isEqualTo: FirebaseAuth.instance.currentUser!.uid.toString())
.get();
_petsList = List.from(data.docs.map((doc) => Pet.fromSnapshot(doc)));
for (var pet in _petsList) {
tiles.add(PetTile(pet: pet));
}
for (var tile in tiles) {
slidables.add(
Slidable(
child: tile,
endActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (value) async {
var ref = FirebaseFirestore.instance
.collection('pets')
.where('id', isEqualTo: tile.pet.id)
.get();
// Deleting...
},
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
],
),
),
);
}
}
You can get the id of the document by doing the following steps:
Add await infront when you're accessing the conditioned data from firebase collection.. in your case in front of FirebaseFirestore.instance
*This will return a QuerySnapshot rather than a Future instance of the same.
You need to get the doc and the id of that doc.. write:
final id= ref.docs[0].id
*Using first index(0) because i am assuming that only one pet id matches with other pet id.
since you have the id now.. you can perform the delete function

How to get document ID for a specific document?

I would like to get the document id of a specific document when I put its name in a function.
Here is my function:
void _getId(town) {
var data = FirebaseFirestore.instance
.collection('towns')
.where('town_name', isEqualTo: town)
.snapshots();
print(data.id);}
Here is my button:
ElevatedButton(
onPressed: () => _getId(_townChosen), child: Text('Get ID')),
The value _townChosen is a string which corresponds to the field town_name in the database. In the complete program, I get the value from a dropdown button, this part works well.
Here is the database
All documents have an town_name field.
I need the id of the chosen town and send it in others widgets to use its subcollection. Please can you help me to get the id?
First, create a variable called townid, and change your function to async, and use a stateful widget to update it, and use get instead of snapshots:
String townId = 'Get ID';
void _getId(town) async {
var data = await FirebaseFirestore.instance
.collection('towns')
.where('town_name', isEqualTo: town)
.get();
setState(() {
townId = data.docs[0].id; //because the query returns a list of docs, even if the result is 1 document. You need to access it using index[0].
});
print(townId);
}
In your button:
ElevatedButton(onPressed: () => _getId(_townChosen), child: Text(townId)),

How exactly to merge multiple streams in firebase firestore

Before you say this is a duplicate question or that I should use nested stream builders, please hear me out.
I am designing a social media type application. And I want users to receive updates whenever someone they are following posts something in their "my Followers Posts" collection.
In the app, the app will check firebase rtdb for the current user's following list(the people he is following) and make a list of their uids.
I plan on using said list to create a list of streams (ordered by time, of course) and merge them into one stream that shall then be fed into a stream builder on the private feed page.
On this page, the user will be able to easily follow what their people of interest have been posting.
I figured such a system is a lot more cost efficient than every user having a document in the "private Feed" collection and whenever someone posts something, the app reads their list of followers and then promptly posts an update in each and every one of their private feeds.
Because... Picture someone with 2 million followers. That's 2 million writes instantly. And later on, 2 million reads.
I figured it's a lot more cost efficient for the poster to just put the post in their "publicFeed" and the different followers simply listen in onto that feed and keep up to tabs with them.
But.. This requires implementing a merging of multiple streams (more than 2). How do I do this?
I have tried reading into RxDart but it is total Greek to me. I am relatively a beginner in dart. I've only been coding for about 5 months now.
You can use StreamGroup from the async package: https://pub.dev/documentation/async/latest/async/StreamGroup-class.html to group events from multiple streams - it's well documented and maintained by the dart team. If you have no RxDart experience this is a good choice. It does not have all the features of rx but for a beginner it should be easier to wrap your head around this
I recently had a similar case, and what I suggest You to do is this
(I'm using cloud firestore, but I'm sure You have your streams already written so the important part is the usage of multiple streams):
You have to add this plugin to pub spec.yaml:
https://pub.dev/packages/rxdart
Here is the repository for (in your case posts, let's say newPosts, oldPosts):
class PostRepository {
static CollectionReference get collection => yourCollectionRef;
static Stream<List<Post>> newPosts() {
Query query = collection
.where('Your condition like was viewed', isEqualTo: false)
.orderBy('updateDate', descending: true)
.limit(50);
return query.snapshots().map<List<Post>>((querySnapshot) {
final _newPosts = querySnapshot.documents.map((doc) {
final post = Post.fromDoc(doc);
return post;
}).where((p) => p != null);
return _newPosts
});
}
static Stream<List<Post>> oldPosts() {
Query query = collection
.where('Your condition like was viewed', isEqualTo: true)
.orderBy('updateDate', descending: true)
.limit(50);
return query.snapshots().map<List<Post>>((querySnapshot) {
final _oldPosts = querySnapshot.documents.map((doc) {
final post = Post.fromDoc(doc);
return post;
}).where((p) => p != null);
return _oldPosts
});
}
}
Then to get multiple streams (those two from above combined), do like this in your widget class:
IMPORTANT! you have to import this - import 'package:rxdart/streams.dart';
List<Post> newPosts;
List<Post> oldPosts;
Widget _pageContent() {
return SingleChildScrollView(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: newPosts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(newPosts[index].title)
);
}
),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: oldPosts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(oldPosts[index].title)
);
}
)
]
)
);
}
Widget _posts() {
return StreamBuilder(
stream: CombineLatestStream.list([
PostRepository.getNewPosts(),
PostRepository.getOldPosts()
]),
builder: (context, snap) {
if (snap.hasError) {
debugPrint('${snap.error}');
return ErrorContent(snap.error);
} else if (!snap.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
newPosts = snap.data[0];
oldPosts = snap.data[1];
return _pageContent();
}
);
}
I wrote the code like from head, so there may be some small errors, but I hope You get the point, enjoy :)

Favoriting Items in Flutter Using Sqlite

I want to be able to favorite/mark items in a already populated local sqlite database to display on a "favorites page" that shows a list of favorited items.
In this sqlite database, I have a table of genres and a table of books relating to a specific genre. I am not using a model "book" class to display book information in a list. I have using a rawQuery to place a ROW of book information into a List to display that book information in a bookView page. Like so..
Future getDescriptionData() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "asset_sample_sqlite.db");
ByteData data =
await rootBundle.load(join("assets", "sample_sqlite.db"));
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await new File(path).writeAsBytes(bytes);
Database db = await openDatabase(path);
await db
.rawQuery('SELECT * FROM books WHERE id = "${widget.id}"') // id passed on from previous page where it shows a list of books
.then((results) {
setState(() {
loading = false;
initialized = true;
bookDescription = results;
});
then I display the information like so..
body: Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: !loading
? new ListView.builder(
itemCount: bookDescription.length,
itemBuilder: (BuildContext context, int index) {
return new Card(
elevation: 2.0,
child: new ListTile(
title: new Text(${bookDescription[index]['title']}",
textAlign: TextAlign.center,
),
subtitle: new Text(
"${bookDescription[index]['summary']}",
),
),
),
),
);
},
)
What is the best method to include an iconButton in this card, and then have an option to favorite this "book" to add it to a shortlist of books that are favorited in a different favorite book list page?
You could create a Favorites table that only holds id values of the books table that have been favored. You should read that table also in getDescriptionData (or a similar getFavoriteData method) so that you have a list of favored book ids in memory, available when the build function is called. In the Card add a leading argument with an Icon representing a filled or unfilled star (depending on whether the bookDescription[index]['id'] is in the favored list) and wrap it in a GestureDetector where in its onTap you add or remove the book id from the list in memory and in your SQL table, then call setState to rebuild the list with the new favorite information.

Resources