I want to be able to bulk add records to a nosql database in Vapor 3.
This is my Struct.
struct Country: Content {
let countryName: String
let timezone: String
let defaultPickupLocation: String
}
So I'm trying to pass an array of JSON objects but I'm not sure how to structure the route nor how to access the array to decode each one.
I have tried this route:
let countryGroup = router.grouped("api/country")
countryGroup.post([Country.self], at:"bulk", use: bulkAddCountries)
with this function:
func bulkAddCountries(req: Request, countries:[Country]) throws -> Future<String> {
for country in countries{
return try req.content.decode(Country.self).map(to: String.self) { countries in
//creates a JSON encoder to encode the JSON data
let encoder = JSONEncoder()
let countryData:Data
do{
countryData = try encoder.encode(country) // encode the data
} catch {
return "Error. Data in the wrong format."
}
// code to save data
}
}
}
So how do I structure both the Route and the function to get access to each country?
I'm not sure which NoSQL database you plan on using, but the current beta versions of MongoKitten 5 and Meow 2.0 make this pretty easy.
Please note how we didn't write documentation for these two libraries yet as we pushed to a stable API first. The following code is roughly what you need with MongoKitten 5:
// Register MongoKitten to Vapor's Services
services.register(Future<MongoKitten.Database>.self) { container in
return try MongoKitten.Database.connect(settings: ConnectionSettings("mongodb://localhost/my-db"), on: container.eventLoop)
}
// Globally, add this so that the above code can register MongoKitten to Vapor's Services
extension Future: Service where T == MongoKitten.Database {}
// An adaptation of your function
func bulkAddCountries(req: Request, countries:[Country]) throws -> Future<Response> {
// Get a handle to MongoDB
let database = req.make(Future<MongoKitten.Database>.self)
// Make a `Document` for each Country
let documents = try countries.map { country in
return try BSONEncoder().encode(country)
}
// Insert the countries to the "countries" MongoDB collection
return database["countries"].insert(documents: documents).map { success in
return // Return a successful response
}
}
I had a similar need and want to share my solution for bulk processing in Vapor 3. I’d love to have another experienced developer help refine my solution.
I’m going to try my best to explain what I did. And I’m probably wrong.
First, nothing special in the router. Here, I’m handling a POST to items/batch for a JSON array of Items.
router.post("items", "batch", use: itemsController.handleBatch)
Then the controller’s handler.
func createBatch(_ req: Request) throws -> Future<HTTPStatus> {
// Decode request to [Item]
return try req.content.decode([Item].self)
// flatMap will collapse Future<Future<HTTPStatus>> to [Future<HTTPStatus>]
.flatMap(to: HTTPStatus.self) { items in
// Handle each item as 'itm'. Transforming itm to Future<HTTPStatus>
return items.map { itm -> Future<HTTPStatus> in
// Process itm. Here, I save, producing a Future<Item> called savedItem
let savedItem = itm.save(on: req)
// transform the Future<Item> to Future<HTTPStatus>
return savedItem.transform(to: HTTPStatus.ok)
}
// flatten() : “Flattens an array of futures into a future with an array of results”
// e.g. [Future<HTTPStatus>] -> Future<[HTTPStatus]>
.flatten(on: req)
// transform() : Maps the current future to contain the new type. Errors are carried over, successful (expected) results are transformed into the given instance.
// e.g. Future<[.ok]> -> Future<.ok>
.transform(to: HTTPStatus.ok)
}
}
Related
I have a dynamic array (named "items") of users I follow:
["user5#t.co", " user6#t.co"]
and I'm essentially retrieving these user's posts using:
for i in 0..< items.count{
db.collection("users").document("\(items[i])").collection("posts").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.posts = documents.map { QueryDocumentSnapshot -> Post in
let longitudeVar = data["longitude"] as? String ?? ""
let latitudeVar = data["latitude"] as? String ?? ""
return Post(id: .init(), longitudeVAR: longitudeVAR, latitudeVAR: latitudeVAR)
}
}
}
I'm trying to draw information from both users at the same time but the issue I'm having is that this only draws post information (longitudeVar & latitudeVar) for ONE user OR the other- and it seems to randomly pick between user5#t.co and user6#t.co. Any suggestions? Also I apologize if this is a basic question or if my code isn't well written- I'm just trying to get this to work. Thanks!
If you want to loop database fetches and coordinate their returns into a single task then you should use a DispatchGroup. It's a very simple API that will simply record how many calls you make to Firestore and give you a completion handler to execute when that many returns eventually come in (at the end of the last one).
Don't use a snapshot listener here because those are for streams of data—all you want is a one-time document grab. If there are lots of documents to parse and they are relatively big then I would consider using a background queue so the UI doesn't stutter when this is going on.
var posts = [Post]()
let dispatch = DispatchGroup() // instantiate dispatch group outside loop
for item in items {
dispatch.enter() // enter on each iteration
// make a get-documents request, don't add a continuously-updating snapshot listener
db.collection("users").document(item).collection("posts").getDocuments { (querySnapshot, error) in
if let documents = querySnapshot?.documents {
// fill a local array, don't overwrite the target array with each user
let userPosts = documents.map { QueryDocumentSnapshot -> Post in
let longitudeVar = data["longitude"] as? String ?? ""
let latitudeVar = data["latitude"] as? String ?? ""
return Post(id: .init(), longitudeVAR: longitudeVAR, latitudeVAR: latitudeVAR)
}
self.posts.append(userPosts) // append local array to target array
} else if let error = error {
print(error)
}
dispatch.leave() // always leave, no matter what happened inside the iteration
}
}
/* this is the completion handler of the dispatch group
that is called when all of the enters have equaled all
of the leaves */
dispatch.notify(queue: .main) {
self.tableView.reloadData() // or whatever
}
I'm trying to create a basic following algorithm using Swift and Firebase. My current implementation is the following:
static func follow(user: FIRUser, userToFollow: FIRUser) {
database.child("users").child(user.uid).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
var dbFollowing: NSMutableArray! = snapshot.value!["Following"] as! NSMutableArray!
dbFollowing?.addObject(userToFollow.uid)
self.database.child("users/"+(user.uid)+"/").updateChildValues(["Following":dbFollowing!])
//add user uid to userToFollows followers array in similar way
}) { (error) in
print("follow - data could not be retrieved - EXCEPTION: " + error.localizedDescription)
}
}
This retrieves the array of from Firebase node Following, adds the uid of userToFollow, and posts the new array to node Following. This has a few problems:
It is not synchronized so if it is called at the same time on two devices one array will overwrite the other and followers will not be saved.
If there are no followers it cannot deal with a nil array, the program will crash (not the main concern, I can probably address with optionals).
I was wondering what the best practice might be to created a synchronized array of uid/tokens for user followers or post likes. I found the following links, but none seem to directly address my problem and seem to carry other problems with it. I figured it would be wise to ask the community with experience instead of Frankensteining a bunch of solutions together.
https://firebase.googleblog.com/2014/05/handling-synchronized-arrays-with-real.html
https://firebase.google.com/docs/database/ios/save-data (the save data as transaction section)
Thanks for your help!
Thanks to Frank, I figured out a solution using runTransactionBlock. Here it is:
static func follow(user: FIRUser, userToFollow: FIRUser) {
self.database.child("users/"+(user.uid)+"/Following").runTransactionBlock({ (currentData: FIRMutableData!) -> FIRTransactionResult in
var value = currentData?.value as? Array<String>
if (value == nil) {
value = [userToFollow.uid]
} else {
if !(value!.contains(userToFollow.uid)) {
value!.append(userToFollow.uid)
}
}
currentData.value = value!
return FIRTransactionResult.successWithValue(currentData)
}) { (error, committed, snapshot) in
if let error = error {
print("follow - update following transaction - EXCEPTION: " + error.localizedDescription)
}
}
}
This adds the uid of userToFollow to the array Following of user. It can handle nil values and will initialize accordingly, as well as will disregard the request if the user is already following the uid of userToFollow. Let me know if you have any questions!
Some useful links:
The comments of firebase runTransactionBlock
The answer to Upvote/Downvote system within Swift via Firebase
The second link I posted above
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 6 years ago.
Improve this question
I'm looking to update an array called 'events' from an api.
Any ideas? Thanks in advance!
var events = [AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url : String = "http://api/tickets"
let request : NSMutableURLRequest = NSMutableURLRequest()
request.URL = NSURL(string: url)
request.HTTPMethod = "GET"
NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data : NSData?, response : NSURLResponse?, error : NSError?) in
dispatch_async(dispatch_get_main_queue()) {
// Main thread
self.tableView.reloadData()
}
}
}
So you've got your data back, now you need to convert the data into an object that you can work with. Your event array is an array of AnyObject, which can be cumbersome to work with. Instead, I would recommend that you create a model for these objects.
Since I don't know what your data looks like I'll just make up a model, you'll need to edit this to suit your needs:
struct Ticket {
let id : Int
let description : String
// pass your data object directly into your initializer.
init?(data: [String:AnyObject]) {
guard let itemId = data["id"] as? Int else { return }
guard let itemDesc = data["description"] as? String else { return }
id = itemId
description = itemDesc
}
}
// note: this initializer will fail (by design) if you pass in an
// object that doesn't have the proper requirements. You can exchange the
// guards for if-lets to avoid this behavior
Now, rather than using an array of AnyObject, you work specifically with your modeled item:
// use your model for easy data access
var events = [Ticket]()
Next you update your NSURLSession's closure to create and append your new model object to your array:
NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data : NSData?, response : NSURLResponse?, error : NSError?) in
// make sure you have data
guard let returnedData = data else {
print("no data was returned")
return
}
do {
// convert your object to JSON data... the following code
// may differ depending on how your JSON is formed. However,
// the concept is still the same
// get array of dictionaries
let jsonObject = try NSJSONSerialization.JSONObjectWithData(returnedData, options: .MutableLeaves) as! [[String: AnyObject]]
// loop over your array and create Ticket objects
jsonObject.forEach { item in
var ticket = Ticket(item)
// append your tickets to the array
events.append(ticket)
}
} catch let error {
print(error)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
Let's say, that what is returned to you is Array of tickets and only array of tickets. If you want to get this array, as [AnyObject] you could write:
if let arrayOfAnyObjects = data as? [AnyObject] {
events = arrayOfAnyObjects
}
in this way, you will try to cast data from type NSData to type [AnyObject] if possible. And if it succeed, you will assign it.
You have the right idea. What part don't you understand?
How you parse the response from the server depends on the format of the data. Is it JSON? XML? Some other format?
EDIT:
Ok, you said the data from the server is in JSON format. So inside the completion block of your data request, use NSJSONSerialization to convert your data to a collection object. It looks like Dan gave a pretty complete explanation on how to do that while I was away from my computer.
I am trying to do two queries to Parse in my PFQueryTableViewController. I am trying to do one query to obtain all of the objects relating to the currentUser and then place those objects into an array. After the array is filled with its objects, I would like to do another query from
override func queryForTable() -> PFQuery { }.
This second query will use the objects from the array that was created from the first query, in order to retrieve objects of its own.
I don't know how to make the first query execute before the second. As of right now, the second query will execute before the first one before it has a chance to fill the array and as a result messes up the query for the second one. The following is what i have for my code at this point.
class PFNewsFeedTableViewController: PFQueryTableViewController {
var leadersArray : NSMutableArray = NSMutableArray()
override func viewDidAppear(animated: Bool) {
leadersArray.removeAllObjects()
var findLeaders = PFQuery(className: "Follow")
findLeaders.whereKey("follower", equalTo: PFUser.currentUser()!)
findLeaders.orderByDescending("createdAt")
findLeaders.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
//If no error
if error == nil {
println("Successfully retrieved \(objects!.count) leaders.")
// Do something with the found objects
if let objects = objects as? [PFObject] {
for object in objects {
self.leadersArray.addObject(object.objectForKey("leader")!)
}
}
}
self.tableView.reloadData()
self.queryForTable()
}
}
override func queryForTable() -> PFQuery {
var findContent = PFQuery(className: "Content")
findContent.whereKey("user", containedIn: self.leadersArray as [AnyObject])
findContent.orderByDescending("createdAt")
return findContent
}
}
Is there a way to determine the order in which these queries get executed? Any help to this problem would be appreciated.
Yes, you can.
Let me first start by saying that it does sound like you should try to maybe rethink your data model, as executing two queries in succession like this is often a sign of focusing too much on traditional data modeling rather than the more pragmatic nosql-style of modeling where you (especially for mobile apps) should model for queries rather than normalisation.
If you were to solve your case like you said, you can employ the Bolts framework. No need to install this, as this is already installed as part of Parse (which uses Bolts behind the scenes).
It could then look something like this (just a quick write-up. Will probably not work out of the box, but you should be able to make it fit):
var findLeaders = PFQuery(className: "Follow")
findLeaders.whereKey("follower", equalTo: PFUser.currentUser()!)
findLeaders.orderByDescending("createdAt")
findLeaders.findObjectsInBackground().continueWithBlock {
(task: BFTask!) -> AnyObject! in
//If no error
if task.error != nil {
print("Something went wrong...")
} else {
// Do something with the found objects
if let objects = task.result as? [PFObject] {
print("Successfully retrieved \(objects.count) leaders.")
for object in objects {
self.leadersArray.addObject(object.objectForKey("leader")!)
}
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
self.queryForTable()
return nil
}
I figured out the problem and it looks like this
override func queryForTable() -> PFQuery {
var findLeaders = PFQuery(className: "Follow")
findLeaders.whereKey("follower", equalTo: PFUser.currentUser()!)
findLeaders.orderByDescending("createdAt")
var findContent = PFQuery(className: "Content")
findContent.whereKey("user", matchesKey: "leader", inQuery: findLeaders)
findContent.orderByDescending("createdAt")
return findContent
}
I need to put data in the array using a query, but it only works in the block. I searched here and found out that it's because the findObjectsInBackgroundWithBlock is asynchronous, but how can I make it synchronous then?
var cities = [String]()
func loadCityArray() {
let citiesVisited = PFQuery(className: "Trips")
citiesVisited.whereKey("userId", equalTo: (PFUser.currentUser()?.objectId)!)
citiesVisited.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
for object in objects {
let city = object["cityId"] as! String
let query = PFQuery(className: "Cities")
query.whereKey("objectId", equalTo: city)
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let objects = objects {
for object in objects {
self.cities.append(object["cityName"] as! String)
}
}
})
}
}
You do not want to make that synchonous on the main thread. That is strongly discouraged.
You rather want to store the things you need from the request in instance variables and tell the corresponding View Controller new values are present in the block.
edit:
Suppose you have an object waiting to use the data:
var chart : DataConsumer?
In the block where you get the data,
chart.useData(data)
edit 2:
the useData function should keep track of changes in the data set and make use of the information that new data arrived. For example, by displaying it.