I'm creating a database in Firebase to manage a local basketball league, for the first part of development I want to work on match management, mainly scoring and foul registration.
The thing with scoring is that a player can score 1, 2 or 3 points and there are different kinds of fouls, like regular fouls and technical fouls, I want to be able to differentiate between those.
Also, a small detail is that a person can play for different teams in different divisions
Here is my idea for the data structure in Firebase:
divisions:{
division1:{
name:"first division"
teams:{
team1:true
team2:true
}
}
}
teams:{
team1:{
name:"Team 1"
division: division1
players:{
player1:true
player2:true
}
matches:{
match1: true
}
}
}
players:{
player1:{
name:"Player 1"
phoneNumber:"555-XXXX"
address:"123 address"
teams:{ //A player can play for different teams in different divisions
team1:true
team2:true
}
}
}
matches:{
match1:{
date:10-20-2019
court:"West Park"
referee:"John Doe"
players:{
player1:{
/*Should I store points scored and fouls comitted in here and
the players collection?*/
}
player2:{...}
}
}
I'm unsure as to where to put the data regarding points and fouls, in the future I plan to use the database to create statistics and such, but in the meantime I just want to have a registry for matches including the players, their points score and fouls.
There is no singular correct way to model data in a NoSQL database. It all depends on the use-cases of the app you want to build. In fact, when using a NoSQL database, it is quite common to adapt your data model as you add new use-cases to your app.
On your current model, I would add an additional top-level data structure to store information about each individual match. Something like this:
matches: {
matchid1: {
teams: {
team1: true,
team2: true
},
events: {
"-Lasdkjhd31": {
time: "2m44",
type: "foul",
player: "player1id",
team: "team1"
}
}
}
}
But as said, that is dependent on the use-cases of the app. So my structure above allows the storing of event information for each match, which clearly would be useful if you want to show a timeline of what happened in each match.
Beyond general guidance, it's hard to be concrete. I do recommend that you read/watch these though:
NoSQL data modeling
Firebase for SQL developers
The Firebase documentation on data modeling
Getting to know Cloud Firestore (which is about Firestore, but the same logic often applies to Realtime Database too)
Related
I'm trying to figure out the security rules for someone adding/removing votes in a Firestore database. The document is setup like this
{
meetingTitle: "Big Team Meeting",
possibleMeetingTimes: {
1548694800000: {
user-id-1: "The best time for me.",
user-id-2: "I'm free at this time."
},
1548694900000: {
user-id-3: "I have to pick up my kids at the other time."
}
}
}
I want a user to be able to suggest a new date, add a vote to a date, and remove their vote from a date. They should not be able to remove/edit a date not associated with their request.auth.uid, nor should they be able to add a vote not associated with their request.auth.uid.
I'm not tied to this data setup if there is a better way to structure this data that would make my security requests easier to setup. I'm fairly new to Firestore security rules, so advice is welcome.
Say that I have node user, item and user_items used to join them.
Typically one would(as advised in official documents and videos) use such a structure:
"user_items": {
"$userKey": {
"$itemKey1": true,
"$itemKey2": true,
"$itemKey3": true
}
}
I would like to use the following structure instead:
"user_items": {
"$userKey": {
"$itemKey1": 1494912826601,
"$itemKey2": 1494912826602,
"$itemKey3": 1494912826603
}
}
with values being a timestamp value. So that i can order them by creation date also while being able to tell the associated time. Seems like killing two birds with one stone situation. Or is it?
Any down sides to this approach?
EDIT: Also I'm using this approach for the boolean fields such as: approved_at, seen_at,... etc instead of using two fields like:
"some_message": {
"is_seen": true,
"seen_timestamp": 1494912826602,
}
You can model your database in every way you want, as long as you follow Firebase rules. The most important rule is to have the data as flatten as possible. According to this rule your database is structured correctly. There is no 100% solution to have a perfect database but according to your needs and using one of the following situations, you can consider that is a good practice to do it.
1. "$itemKey1": true,
2. "$itemName1": true,
3. "$itemKey1": 1494912826601,
4. "$itemName1": 1494912826601,
What is the meaning of "$itemKey1": 1494912826601,? Beacause you already have set a timestamp, means that your item was uploaded into your database and is linked to the specific user, which means also in other words true. So is not a bad approach to do something like this.
Hope it helps.
Great minds must think alike, because I do the exact same thing :) In my case, the "items" are posts that the user has upvoted. I use the timestamps with orderBy(), along with limitToLast(50) to get the "last 50 posts that the user has upvoted". And from there they can load more. I see no downsides to doing this.
My application requires a monitoring service which I am hosting on Heroku which monitors, corrects, and modifies data based on changes to the users account. However there are some fields on the users account which are updated at rapid speeds, and having all of these accounts spam my monitoring service with data is very inefficient.
Is it possible to ignore fields when listening to child_changed so that the callback is not fired and the data is never downloaded? Depending on the users speed, I could be downloading hundreds of accounts a few times a second on my monitoring application and that's just something that will go down in flames.
Here's an example of what I'm looking for:
{
name: 'Jimmy',
birthday: 'Mar 23, 1976',
biography: 'Random text',
interests: {
// ...
}
}
Perhaps I want to ignore the biography field, therefor the child_added feature should download new account information ONLY if one of the following values are changed:
name
brithdate
interests
and the data should be ignored and not downloaded if the biography field is changed, because it's not a field that is relevant to my monitor, and just wastes bandwidth.
Firebase always synchronizes complete nodes. If you want to synchronize only a subset of each node from a list of nodes, you should separate that subset into its own top-level node.
profiles: {
uidJimmy: {
name: 'Jimmy',
birthday: 'Mar 23, 1976',
biography: 'Random text',
}
},
interests: {
uidJimmy: {
// ...
}
}
You'll find this to be a common these when using Firebase (or most other NoSQL databases): you have to model the data for the way that your application wants to consume it. I highly recommend reading this article on NoSQL data modeling.
I have a list of records in firebase which will have a group property with zero or more groups on it. I also have the firebase auth object which will also have zero or more groups on it as well. I would like to set up a .read firebase rule for my records that will check if the two have at lease one group that exists in both lists.
Put another way I have a user that has an array of groups that have been assigned to it. I have some records that also has some list of groups on them that specify what groups the user must have to access them. If the logged in user tries to access the record, I want to make sure that the user has at least one group that the record requires.
On the client I would do something like _.intersect(userGroups, recordGroups).length > 0
I'm not sure how I would do this in a firebase rule expression. It would be cool if it worked something like this.
Record:
{
someData: "test"
groups: ['foo', 'bar']
}
Firebase Auth Object:
{
userName: "Bob",
groups: ['foo', 'bar']
}
Rule Data:
{
"rules": {
"records": {
"$recordId": {
".read": "data.child('groups').intersectsWith(auth.groups)"
}
}
}
}
Thanks.
Update:
I think that if hasChildren() used || instead of && I could put the group names in they key position and check for their existence this way. Something like "data.child('groups').hasChildren(auth.groups, 'or')"
Where Record:
{
someData: "test"
groups: {
'foo': '',
'bar': ''
}
}
Update2:
Based off Kato's comment & link I realize that even if hasChildren could do OR it still wouldn't work quite right. Requests for individual records would work but requests for all records would error if the current user didn't have access to every record.
It is still not clear how you would structure data to make this work. If a record could belong to many groups how would that work? This is a very common scenario(basically how linux group permissions work) so I can't be the only one trying to do this. Anyone have any ideas/examples of how to accomplish this in firebase?
At the current moment, I believe it's impossible. There's a limited number of variables, methods, and operators allowed, listed here:
Firebase Security Rules API
Since function definitions are not allowed in the rules, you can't do anything fancy like call array.some(callback) on an array to do the matching yourself.
You have three options that I know of:
1) Copy data so you don't need to do the check. This is what I did in my project: I wanted some user data (names) available to users that shared a network in their network lists. Originally I wanted to check both member's network lists to see if there was at least one match. Eventually I realized it would be easier to just save each user's name as part of the network data so there wouldn't have to be a user look up requiring this odd permissions. I don't know enough about your data to suggest what you need to copy.
2) Use strings instead of arrays. You can turn one string into a regex (or just save it in regex format) and use it to search the other string for a match.Firebase DB Regex Docs
3) If you have enough weird cases like this, actually run a server that validates the request in a custom fashion. In the DB, just allow permissions to your server. You could use Firebase Cloud Functions or roll your own server that uses the Firebase Admin SDK
Nowadays, there's another possibility: to use Firestore to deliver your content, possibly in sync with the Realtime Database.
In Firestore, you can create rules like this:
function hasAccessTo(permissionList) {
return get(/databases/$(database)/documents/permissions/$(request.auth.uid))
.data.userPermissions.keys().hasAny(permissionList)
}
match /content/{itemId} {
allow read: if hasAccessTo(resource.data.permissions.keys());
}
The following data would allow a read of $CONTENTID by $UID, because the user permissions set intersects with the possible permissions required to access the content (with access123). My scenario is that a piece of content can be unlocked by multiple In-App Purchases.
{
permissions: {
$UID: { userPermissions: { access123:true, access456:true } },
...
},
content: {
$CONTENTID: { ..., permissions: { access123, access789 } },
...
}
}
For a progressive migration, you can keep data in sync between the Realtime Database and Firestore by using a one-way Cloud Function like this for example:
exports.fsyncContent = functions.database
.ref("/content/{itemId}")
.onWrite((snapshot, context) => {
const item = snapshot.after.val();
return admin
.firestore()
.collection("content")
.doc(context.params.itemId)
.set(item);
});
I currently use MySQL, after looking into Document DB it seems like it may be a good move. I do a TON (95%) of querying for single records. As my database gets larger, the time its taking to do this seems to be getting slower. Both reading and writing. I'm curious based on the (simplified) scheme below if it could be a good move to a DocumentDB, and what the layout would be for said schema (i'm a bit new to documentDB)
User
UserID
Username
CreatedDate
Tank
TankID
UserID REF User.UserID
TankName
Awards
Map
MapID
MapName
MapFIle
MapData
MapID REF Map.MapID
TankID REF Tank.TankID
Rank
Color
TimePlayed
Equipment
Everytime a player joins, the data from Tank,MapaData is Queried to gather a full tank object. Every time they die, win an award, kill somebody, or exit the game, the data is then written back out to tank,and mapdata.
The website queries the User table for login, which stores the username and a hash of the password. Once logged in the users are able to modify/delete/create new tanks on the website, which inserts records into the tank/mapdata tables.
The website also stores Top 25 in the World, t25 in map, t25 for each color, t25 for each color for each map.
That's about the only query patterns I can think of at this moment.
Based on the provided information you have the choice of several schema designs (with JSON as examples). I've made some assumptions, such as that more than one tank can be on one map and map data is only linked to a single map. You have to tweak it for your needs. I also try to provide some advantages and disadvantages of every solution.
Option #1 (Single collection)
This should be the easiest, but not the best solution. Here you put everything into one document with extreme "denormalization".
{
"mapname": "map1",
"mapfile": "mapfile1",
"data": {
"rank": "rank1",
"color": "color1",
...
"tanks": [
{
"name": "tank1",
...
"user": {
"name": "user1",
...
}
},
{
...
}
]
}
}
This solution works best when you do a lot of writes, rare updates and reads where you want to get all information together. On the other side it has a lot of disadvantages, such as storing user information directly into your application data (an example would be the password hash).
Option #2 (Two collections)
Put your user data into one collection and the other data into a second collection.
User collection
{
"id": 1,
"username": "user1",
"password": "passwordhash",
...
}
Data collection
{
"mapname": "map1",
"mapfile": "mapfile1",
"data": {
"rank": "rank1",
"color": "color1",
...
"tanks": [
{
"name": "tank1",
...
"user": userId
}
},
{
...
}
]
}
}
This option is a lot better than the first one. First you don't want to have sensitive user data (such as the hash of the password) in a collection with your other data. Also this works better for reads of the user object, because you just retrieve the information you need without skipping a lot of not needed fields. A disadvantage is that heavy write operations on the tank object can become a problem.
Option #3 (Three collections)
The next step could be to move the tanks out of the data collection into their own collection.
User collection
{
"id": 1,
"username": "user1",
"password": "passwordhash",
...
}
Tank collection
{
"name": "tank1",
...
"user": userId
}
Data collection
{
"mapname": "map1",
"mapfile": "mapfile1",
"data": {
"rank": "rank1",
"color": "color1",
...
"tanks": [
idOfTank1,
idOfTank2,
...
]
}
}
This works best for a lot of writes of single objects, such as the tanks, and reading tanks from their collection. This solution has its problems when reading a lot of data together, for example if you want to get a map and all the tanks in that map. In that case you have to resolve the dependencies of the tanks and the map data.
Summary
As seen, schema design is not easy in a document-oriented database. This is the reason why I asked for the query patterns. To come up with a good design you have to know most of the query patterns in advance. To get started, you should create a simple prototype with a design you think makes sense and test your query patterns with some test data. If that works, you can make minor changes to get even better performance. If not, rethink your query patterns and how a better design could look like. Keep in mind that you don't need a full-blown application for that. Most of that can be tested before a single line of code is written, for example with the administration shell of MongoDB or a simple console application in the case of DocumentDB.