I have an application where user can like photos, comment etc. Functionality like Instagram has.
I want to realize users !feedback!, where user can see information, who liked his photos, who started to follow and etc. I don't know actually how should I organize structure of my database in this situation.
My user node snapshot:
My posts node snapshot:
As I can see, I have next option - I should save all actions, which are linked to user, to his node in internal node Feedback. But how can I keep sync this? For example, someone can follow my user, I will add it to this node, user will unfollow, but the record still remains. I think, that it is wrong way.
I have no other idea actually and I can't find anything about that.
Any suggestions and solutions are much appreciated.
EDIT: I need to understand, how to realize this tab of instagram-like apps:
How to retrieve data for it from nodes?
UPD: DB Architecture in my examples is bad (old question). Be carefull (10.11.2017).
First, let's think about how we need to structure our database for this:
There are two very important principles to follow when structuring data for Firebase:
You should save your data the way you want to retrieve it.
You should keep your data structure as flat as possible - avoid nesting.
Point 1 is because Firebase is not a relational database. This means that we need to keep queries simple in order to achieve performance. Making complex queries might require many requests to Firebase.
Point 2 is because of the way Firebase's query model works: If you observe a node, you also get all the children of that node. This means that, if your data is deeply nested, you might get a lot of data you don't need.
So, having those principles in mind, let's take a look at your case. We have users, who have photos. These are the two primary entities of your database.
I can see that, currently, you are keeping your photos as properties of the users. If you want to be able to query photos by user quickly (remember Point 1), this is a good way to do it. However, if we want users to be able to "favorite" photos, a photo should be more than just a link to its Firebase Storage location: It should also hold other properties, such as which users have favorited it. This property should be an array of user IDs. In addition, for each user, you'll want to store which photos are that user's favorites. This might seem like data duplication, but when using Firebase, it's OK to duplicate some data if it'll lead to simpler queries.
So, using a data index such as in the example above, each of your Photos should look like this:
{
id: /* some ID */,
location: /* Firebase Storage URL */,
favorited_by: {
/* some user ID */: true /* this value doesn't matter */,
/* another user ID */: true,
},
/* other properties... */
}
And your user should have a favorites property listing photo IDs. Now, since every photo has a user that "owns" it, we don't need to have a unique ID for every photo, we just need to ensure that no user has two photos with the same ID. This way, we can refer to the photo by a combination of its user ID and its photo ID.
Of course, remember Point 1: If you want to be able to get user info without getting a user's photos, you should have a different property on your root object for photos instead of associating photos with users. However, for this answer, I'll try to stick to your current model.
Based on what I said above, the favorites property of a user would hold an array of values of the format 'userId/photoId'. So, for instance, if a user favorites the photo with ID "3A" of the user with ID "CN7v0A2", their favorites array would hold the value 'CN7v0A2/3A'. This concludes our structure for favorites.
Now, let's look at what some operations you have mentioned would look like under this structure:
User favorites a photo:
We get the user ID of the photo's owner
We get the user ID of the user who is favoriting the photo
We get the ID of the photo
We add the user who is favoriting's ID to the photo's favorited_by array
We add photoOwnerID + "/" photoID to the favoriting user's favorites array
If the user unfavorites the photo later, we just do the opposite: We remove photoOwnerID + "/" + photoID from the user's favorites and we remove the favoriting user's ID from the photo's favorited_by property.
This kind of logic is sufficient to implement likes, favorites, and follows. Both the follower/liker/favoriter and the followee/likee/favoritee should hold references to the other party's ID, and you should encapsulate the "like/favorite/follow" and "unlike/favorite/unfollow" operations so that they keep that database state consistent every time (this way, you won't run into any issues such as the case you mentioned, where a user unfollows an user but the database still holds the "following" record).
Finally, here's some code of how you could do the "Favorite" and "Unfavorite" operations, assuming you have a User model class:
extension User {
func follow(_ otherUser: User) {
let ref = FIRDatabase.database().reference()
ref.child("users/\(otherUser.userId)/followers/")
.child(self.userId).setValue(true)
ref.child("user/\(self.userId)/following/")
.child(otherUser.userId).setValue(true)
}
func unfollow(_ otherUser: User) {
let ref = FIRDatabase.database().reference()
ref.child("users/\(otherUser.userId)/followers/")
.child(self.userId).remove()
ref.child("user/\(self.userId)/following/")
.child(otherUser.userId).remove()
}
}
Using this model, you can get all the follower user IDs for a user querying that user's followers property and using the .keys() method on the resulting snapshot, and conversely for users a given user follows.
Added content: We can build further on this structure in order to add simple logging of actions, which seems to be what you want to have available to the user in the "Feedback" tab. Let's assume we have a set of actions, such as liking, favoriting and following, which we want to show feedback for.
We'll follow point 1 once again: In order to structure feedback data, it is best to store this data in the same way we want to retrieve it. In this case, we will be most often showing a user their own feedback data. This means we should probably store feedback data by user ID. Additionally, following point 2, we should store feedback data as its own table, instead of adding it to the user records. So we should make a new table on our root object, where for each user ID, we store a list of feedback entries.
It should look something like this:
{
feedback: {
userId1: /* this would be an actual user ID */ {
autoId1: /* generated using Firebase childByAutoId */ {
type: 'follow',
from: /* follower ID */,
timestamp: /* Unix time */,
},
autoId2: {
type: 'favorite',
from: /* ID of the user who favorited the photo */
on: /* photo ID */
timestamp: /* Unix time */
},
/* ...other feedback items */
},
userId2: { /* ...feedback items for other user */ },
/* ...other user's entries */
},
/* other top-level tables */
}
In addition, we will need to change the favorites/likes/follows tables. Before, we were just storing true in order to signal that someone liked or favorited a photo or followed a user. But since the value we use is irrelevant, as we only check keys to find what the user has favorited or liked and who they have followed, we can start using the ID of the entry for the like/favorite/follow. So we would change our "follow" logic to this:
extension User {
func makeFollowFeedbackEntry() -> [String: Any] {
return [
"type": "follow",
"from": self.userId,
"timestamp": UInt64(Date().timeIntervalSince1970)
]
}
func follow(_ otherUser: User) {
let otherId = otherUser.userId
let ref = FIRDatabase.database().reference()
let feedbackRef = ref.child("feedback/\(otherId)").childByAutoId()
let feedbackEntry = makeFollowFeedbackEntry(for: otherId)
feedbackRef.setValue(feedbackEntry)
feedbackRef.setPriority(UInt64.max - feedbackEntry["timestamp"])
let feedbackKey = feedbackRef.key
ref.child("users/\(otherUser.userId)/followers/")
.child(self.userId).setValue(feedbackKey)
ref.child("user/\(self.userId)/following/")
.child(otherUser.userId).setValue(feedbackKey)
}
func unfollow(_ otherUser: User, completionHandler: () -> ()) {
let ref = FIRDatabase.database().reference()
let followerRef = ref.child("users/\(otherUser.userId)/followers/")
.child(self.userId)
let followingRef = ref.child("user/\(self.userId)/following/")
.child(otherUser.userId)
followerRef.observeSingleEvent(of: .value, with: { snapshot in
if let followFeedbackKey = snapshot.value! as? String {
// we have an associated follow entry, delete it
ref.child("feedback").child(otherUser.userId + "/" + followFeedbackKey).remove()
} // if the key wasn't a string, there is no follow entry
followerRef.remove()
followingRef.remove()
completionHandler()
})
}
}
This way, we can get a user's "feedback" just by reading the "feedback" table entry with that user's ID, and since we used setPriority, it will be sorted by the most recent entries first, meaning we can use Firebase's queryLimited(toFirst:) to get only the most recent feedback. When a user unfollows, we can easily delete the feedback entry which informed the user that they had been followed. You can also easily add extra fields to store whether the feedback entry has been read, etc.
And even if you were using the other model before (setting "followerId" to true), you can still use feedback entries for new entries, just check if the value as "followerId" is a string as I have done above :)
You can use this same logic, just with different fields in the entry, to handle favorites and likes. When you handle it in order to show data to the user, just check the string in the "type" field to know what kind of feedback to show. And finally, it should be easy to add extra fields to each feedback entry in order to store, for instance, whether the user has seen the feedback already or not.
You can sort of implement what you want by using Firebase Functions. Here's roughly how I would go about implementing it:
All a user's feedback will be stored in /Feedback/userID/, located at the root.
Within this node, have a subnode called eventStream.
Whenever an action occurs, this can be directly added to the user's eventStream, ordered by time.
This action could be of the form: pushID: { actionType:"liked", post:"somePostID", byUser:"someUserId" }
Also include an anti-action subnode (under /Feedback/userID/). Whenever one of these 'anti-action' events occurs (for example: unlike, unfollow etc.), store this under the anti-action node for the corresponding user. This node will essentially act as a buffer for our function to read from.
This anti-action could be of an almost identical form: pushID: { actionType:"unliked", post:"somePostID", byUser:"someUserId" }
Now for the function.
Whenever an anti-action is added to the anti-action node, a function removes this from the anti-action node, finds the corresponding action in the eventStream, and removes this. This can be achieved easily by first querying by "actionType" then "someUserId" and then by "somePostID".
This will ensure that the user's eventStream will always be up to date with the latest events.
Hope this helps! :)
In RestKit is it possible to use identificationAttributes that are actually not part of the JSON response?
My case is the following - I have a service that lists all articles for the currently logged-in user like http://example.com/json/articles.json
My problem is the following - since the application allows multiple users to login, I keep the articles in the database together with the userId for each article. If I set the articleMapping.identificationattributes = #["articleId"], then I have a problem if two users using the device have the same article - it will be overwritten regardless of the userId, because it is not part of the response.
To sum up the facts:
For the JSON request I do not send the userId, it is part of the
server session only, so I think that I cannot use RKRoute
I do the mapping of the article with the user manually after RestKit mapping.
I do not have the userId property as part of the JSON response, it exists only inside the ArticleManagedObject.
Is there a way to inform RestKit that during the mapping, it should check the articleId+userId combination as an identificator? I tried using identificationPredicate with no success.
EDIT:
An example response from the server, when UserA is logged in:
{
"data":{
"articles":[
{
"articleId":1,
"title":"Objective C Basics"
},
{
"articleId":2,
"title":"Xcode Basics"
}
]
}
}
and here is the response when UserB is logged in:
{
"data":{
"articles":[
{
"articleId":1,
"title":"Objective C Basics"
},
{
"articleId":3,
"title":"Java Basics"
}
]
}
}
If UserA logs in, everything is fine. But if UserB logs in from the same device, then article 1 is mapped to UserB, and from now on, the connection between UserA and article 1 is lost.
As I understand from your suggestion, the only solution is to return also the user id from the service, set RKUnionAssignmentPolicy and let RestKit take care of the mapping (currently I am manually making the mapping between articles and users after RestKit).
Another question that I have - is it possible to set the identificationAttributes or identificationPredicate so that it makes a separation between object article 1 for UserA and object article 1 for UserB.
You currently do the user to article mapping outside RestKit, this is fine, but you will need to modify this process a little.
To begin with, I'm assuming here that the article response is the full set of articles for the user. If not then things get more tricky and you'll need to modify the below to account:
Start by getting all of the existing articles for a user. With this we're going to look at what needs to be removed and what needs to be added.
As we iterate through the articles we have received we can check the existing articles for a match, if we find one we have no work to do. If we don't find a match we need to add the relationship to the existing set, which will be a union with any relationship to any other user.
Next we want to remove the list of new articles from the list of the existing articles to get the list of deletions, for these we just need to break the link, again leaving other users unchanged.
I'm attempting to change the type of one custom orgunit to another to correct an error that was made previously.
Doing: GET /d2l/api/lp/1.4/orgstructure/6770
Results in:
{
"Identifier": "6770",
"Name": "Art",
"Code": "ART",
"Type": {
"Id": 101,
"Code": "Department",
"Name": "Department"
}
}
I then take that data and run it through PUT /d2l/api/lp/1.4/orgstructure/6770 as per the documentation however I change the data to be:
{
"Identifier": "6770",
"Path": "/content/",
"Name": "Art",
"Code": "ART",
"Type": {
"Id": 103,
"Code": "Discipline",
"Name": "Discipline"
}
}
Essentially only adding a "Path" property because it issues a 404 without it. As well as changing the type to a Discipline rather than Department. However the object returned is identical to the original without updating any of the type information.
Any suggestions to how to fix this? Deletion and recreation at this point is not a feasible option at all. Because both of these are "custom" org unit types I would imagine an update like this shouldn't be difficult.
This is an oversight in the documentation, combined with a somewhat awkward evolution of the API. The documentation has now been updated to be more clear on this situation:
The update orgunit properties call can only update the Name, Code, or Path properties of an orgunit, not it's Identifier (sensibly) or it's Type. (I do not believe there is a way to update the type of an org unit, once created, even in the Web UI for the LMS -- you likely have to create a new org unit, re-assign parent and children relationships as appropriate, and then drop the old unit.)
Unfortunately, you must provide a valid, good Path for the org unit, and the simple call to fetch a single org unit's properties won't tell you what the current one is.
If you don't already know what the path is, and should be, then you'll need to call the route to fetch back a list of org unit records, find the exact one that matches yours (by Identifier, or by matching on several properties like Code and Name), and then send back that Path dispensed in the record sent back there. (Note that you're strongly advised to scope the call to fetch back a list of org unit records by filtering on type, code, and/or name, and the call is paged, so you may have to proceed with it several times if you don't scope down the call enough, to find the particular org unit record in question.)
I am using the Youtube Javascript client to request a list of the recommendations for the OAuth'd user:
gapi.client.youtube.activities.list({
"part": "snippet",
"home": true,
"maxResults": 50
}).execute(function(data) {
// etc
});
This returns a load of recommendations, as expected, and the recommendations don't appear to change. However, the ID of the recommendation (data.items[k].id) changes (usually towards the end of the ID) each time I make the request.
When I was making a similar call for Google+, the IDs that come back are the same every time for posts etc.
So should these IDs be immutable (i.e. this is a bug in the API) or is my assumption that they are always the same invalid? The documentation says:
The ID that YouTube uses to uniquely identify the activity.
... which doesn't really help one way or the other.
If this is the case, how do you uniquely identify these items?
Assuming you want the video id, you need to specify contentDetails as the part.
gapi.client.youtube.activities.list({
"part": "snippet,contentDetails",
"home": true,
"maxResults": 50
}).execute(function(data) {
// etc
});
I've created a web application that using SurveyMonkey API and gathers the responses to a bunch of surveys I've created with SurveyMonkey. It's a BASIC account, however when I call the method get_responses on a survey it only works for one of my surveys.
I have 6 surveys that have been completed but it returns a json response with null values for 5 of my surveys.
"{"status": 0, "data": [null, null, null, null, null, null]}"
Does anyone know why this is the case?
You need to call get_respondent_list for each individual survey, and then pass a list of respondent ids to get_responses in the parameter 'respondent_ids'. You also need to ensure you pass the correct survey_id to get_responses. If you're getting null values, this means you are either passing invalid respondent ids, or respondent ids that do not correspond to the survey_id you are sending.
See here for more info: https://developer.surveymonkey.com/mashery/get_responses and https://developer.surveymonkey.com/mashery/get_respondent_list