0

Implementing an Android+Web(Angular)+Firebase app, which has a many-to-many relationship: User <-> Widget (Widgets can be shared to multiple users).

Considerations:

  1. List all the Widgets that a User has.
  2. A User can only see the Widgets which are shared to him/her.
  3. Be able to see all Users to whom a given Widget is shared.
  4. A single Widget can be owned/administered by multiple Users with equal rights (modify Widget and change to whom it is shared). Similar to how Google Drive does sharing to specific users.

One of the approaches to implement fetching (join-style), would be to go with this advice: https://www.firebase.com/docs/android/guide/structuring-data.html ("Joining Flattened Data") via multiple listeners. However I have doubts about this approach, because I have discovered that data loading would be worryingly slow (at least on Android) - I asked about it in another question - Firebase Android: slow "join" using many listeners, seems to contradict documentation .

So, this question is about another approach: per-user copies of all Widgets that a user has. As used in the Firebase+Udacity tutorial "ShoppingList++" ( https://www.firebase.com/blog/2015-12-07-udacity-course-firebase-essentials.html ).

Their structure looks like this:

In particular this part - userLists:

  "userLists" : {
    "abc@gmail,com" : {
      "-KBt0MDWbvXFwNvZJXTj" : {
        "listName" : "Test List 1 Rename 2",
        "owner" : "xyz@gmail,com",
        "timestampCreated" : {
          "timestamp" : 1456950573084
        },
        "timestampLastChanged" : {
          "timestamp" : 1457044229747
        },
        "timestampLastChangedReverse" : {
          "timestamp" : -1457044229747
        }
      }
    },
    "xyz@gmail,com" : {
      "-KBt0MDWbvXFwNvZJXTj" : {
        "listName" : "Test List 1 Rename 2",
        "owner" : "xyz@gmail,com",
        "timestampCreated" : {
          "timestamp" : 1456950573084
        },
        "timestampLastChanged" : {
          "timestamp" : 1457044229747
        },
        "timestampLastChangedReverse" : {
          "timestamp" : -1457044229747
        }
      },
      "-KByb0imU7hFzWTK4eoM" : {
        "listName" : "List2",
        "owner" : "xyz@gmail,com",
        "timestampCreated" : {
          "timestamp" : 1457044332539
        },
        "timestampLastChanged" : {
          "timestamp" : 1457044332539
        },
        "timestampLastChangedReverse" : {
          "timestamp" : -1457044332539
        }
      }
    }
  },

As you can see, the copies of shopping list "Test List 1 Rename 2" info appears in two places (for 2 users).

And here is the rest for completeness:

{
  "ownerMappings" : {
    "-KBt0MDWbvXFwNvZJXTj" : "xyz@gmail,com",
    "-KByb0imU7hFzWTK4eoM" : "xyz@gmail,com"
  },
  "sharedWith" : {
    "-KBt0MDWbvXFwNvZJXTj" : {
      "abc@gmail,com" : {
        "email" : "abc@gmail,com",
        "hasLoggedInWithPassword" : false,
        "name" : "Agenda TEST",
        "timestampJoined" : {
          "timestamp" : 1456950523145
        }
      }
    }
  },
  "shoppingListItems" : {
    "-KBt0MDWbvXFwNvZJXTj" : {
      "-KBt0heZh-YDWIZNV7xs" : {
        "bought" : false,
        "itemName" : "item",
        "owner" : "xyz@gmail,com"
      }
    }
  },
  "uidMappings" : {
    "google:112894577549422030859" : "abc@gmail,com",
    "google:117151367009479509658" : "xyz@gmail,com"
  },
  "userFriends" : {
    "xyz@gmail,com" : {
      "abc@gmail,com" : {
        "email" : "abc@gmail,com",
        "hasLoggedInWithPassword" : false,
        "name" : "Agenda TEST",
        "timestampJoined" : {
          "timestamp" : 1456950523145
        }
      }
    }
  },

  "users" : {
    "abc@gmail,com" : {
      "email" : "abc@gmail,com",
      "hasLoggedInWithPassword" : false,
      "name" : "Agenda TEST",
      "timestampJoined" : {
        "timestamp" : 1456950523145
      }
    },
    "xyz@gmail,com" : {
      "email" : "xyz@gmail,com",
      "hasLoggedInWithPassword" : false,
      "name" : "Karol Depka",
      "timestampJoined" : {
        "timestamp" : 1456952940258
      }
    }
  }
}

However, before I jump into implementing a similar structure in my app, I would like to clarify a few doubts.

Here are my interrelated questions:

  1. In their ShoppingList++ app, they only permit a single "owner" - assigned in the ownerMappings node. Thus no-one else can rename the shopping list. I would like to have multiple "owners"/admins, with equal rights. Would such a keep-copies-per-user structure still work for multiple owner/admin users, without risking data corruption/"desynchronization" or "pranks"?
  2. Could data corruption arise in scenarios like this: User1 goes offline, renames Widget1 to Widget1Prim. While User1 is offline, User2 shares Widget1 to User3 (User3's copy would not yet be aware of the rename). User1 goes online and sends the info about the rename of Widget1 (only to his own and User2's copies, of which the client code was aware at the time of the rename - not updating User3's copy). Now, in a naive implementation, User3 would have the old name, while the others would have the new name. This would probably be rare, but still worrying a bit.
  3. Could/should the data corruption scenario in point "2." be resolved via having some process (e.g. on AppEngine) listening to changes and ensuring proper propagation to all user copies?
  4. And/or could/should the data corruption scenario in point "2." be resolved via implementing a redundant listening to both changes of sharing and renaming, and propagating the changes to per-user copies, to handle the special case? Most of the time this would not be necessary, so it could result in performance/bandwidth penalty and complicated code. Is it worth it?
  5. Going forward, once we have multiple versions deployed "in the wild", wouldn't it become unwieldy to evolve the schema, given how much of the data-handling responsibility lies with the code in the clients? For example if we add a new relationship, that the older client versions don't yet know about, doesn't it seem fragile? Then, back to the server-side syncer-ensurerer process on e.g. AppEngine (described in question "3.") ?
  6. Would it seem like a good idea, to also have a "master reference copy" of every Widget / shopping-list, so as to give good "source of truth" for any syncer-ensurerer type of operations that would update per-user copies?
  7. Any special considerations/traps/blockers regarding rules.json / rules.bolt permissions for data structured in such a (redundant) way ?

PS: I know about atomic multi-path updates via updateChildren() - would definitely use them.

Any other hints/observations welcome. TIA.

Community
  • 1
  • 1
KarolDepka
  • 8,318
  • 10
  • 45
  • 58
  • "I would value a general judgement of the ShoppingList++ example - is it an example worth following or should I be skeptical." That is going to be incredibly broad and highly subjective, so I'm inclined to vote to close. You might want to remove it. – Frank van Puffelen Mar 16 '16 at 21:53
  • 7 questions in a single post is too much. It also is *way* too broad. Please narrow it down to a single question that can be answered in a reasonable amount of time/space. – Frank van Puffelen Mar 16 '16 at 21:55
  • See [how to ask](http://stackoverflow.com/help/how-to-ask) – Kato Mar 16 '16 at 22:04
  • 1
    My head exploded. However, there are some good question(s) within the post. I ask the OP to please narrow down the questions to one per post, and limit the scope of the question, sample code, and Firebase structure. Do that and we can help. – Jay Mar 16 '16 at 22:50
  • @FrankvanPuffelen - I've removed the part asking about ShoppingList++ credibility in general. The rest of the questions should still hold. Apart from there being too many questions in one post as some people suggested - which I will try to split. – KarolDepka Mar 17 '16 at 01:12
  • @Kato: regarding "how-to-ask" - which part(s) specifically? I don't think the question is wrong on every principle... – KarolDepka Mar 17 '16 at 19:06

1 Answers1

0

I suggest having only one copy of a widget for the entire system. It would have an origin user ID, and a set of users that have access to it. The widget tree can hold user permissions and change history. Any time a change is made, a branch is added to the tree. Branches can then be "promoted" to the "master" kind of like GIT. This would guarantee data integrity because past versions are never changed or deleted. It would also simplify your fetches... I think :)

{ 
  users:[
    bob:{
      widgets:[
        xxx:{
           widgetKey: xyz,
           permissions: *,
           lastEdit... 
        }
      ]
    }
    ...
  ]
  widgets:[
    xyz:{
       masterKey:abc,
       data: {...},
       owner: bob,
    },
    ...
  ]
  widgetHistory:[
    xyz:[
      v1:{
         data:{...},
      },
      v2,
      v3
    ]
    123:[
       ...
    ],
    ...
  ]
 }
Aaron Franco
  • 1,560
  • 1
  • 14
  • 19
  • Thanks. But how do I efficiently read all the Widgets that a given (currently logged-in) User has? Can you paste some json-s with example structure? Any link to a similar approach? For how long have you been using Firebase, BTW? – KarolDepka Mar 16 '16 at 21:12
  • You can keep a separate tree of reference IDs to access the widgets directly for that user. – Aaron Franco Mar 16 '16 at 21:13
  • Thanks. I've considered this approach (apart from the versioning idea), but it seems too slow to fetch (often as much as 6 seconds for just 100 items) - http://stackoverflow.com/questions/35996865/firebase-android-slow-join-using-many-listeners-seems-to-contradict-document . – KarolDepka Mar 17 '16 at 01:14
  • What is `masterKey:abc` ? – KarolDepka Mar 17 '16 at 02:14
  • That would be the key for the version that is actively in use. – Aaron Franco Mar 17 '16 at 02:16