Introducing multi-location updates and more

Today we released new versions of our Android (2.4.0), iOS (2.4.0), and JavaScript (2.3.0) clients. You can read the individual changelogs for details (Android, iOS, web), but we wanted to expand on three highly requested features shipping with this release: atomic multi-location writes, ordering queries by deep paths, and iOS 9 bitcode support. Let’s jump in!

Atomic Writes Across Multiple Locations

Firebase has always supported updating multiple children of a node atomically. For example, let’s assume the /user path in our Firebase database contains the following data:

{
  "user": {
    "name": "Samantha",
    "age": 25
  }
}

Using the pre-existing atomic update support, we can change the user’s age and add a city without updating their name:

JavaScript

var ref = new Firebase("https://.firebaseio.com");
var userRef = ref.child("user");
var newUserData = {
  "age": 30,
  "city": "Provo, UT"
};
ref.child("user").update(newUserData);  

Java

Firebase ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com");
Firebase userRef = ref.child("user");
Map newUserData = new HashMap();
newUserData.put("age", 30);
newUserData.put("city", "Provo, UT");
userRef.updateChildren(newUserData);  

Objective-C

Firebase *ref = [[Firebase alloc] initWithUrl:@"https://<YOUR-FIREBASE-APP>.firebaseio.com"];
Firebase *userRef = [ref childByAppendingPath: @"user"];
NSDictionary *newUserData = @{
    @"age": 30,
    @"city": "Provo, UT"
};
[userRef updateChildValues: newUserData];

Swift

let ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com")
let userRef = ref.childByAppendingPath("user")
let newUserData = ["age": 30, "city": "Provo, UT"]
userRef.updateChildValues(newUserData)

However this was limited to only atomically updating direct children. Writing to completely separate locations was not possible. For example, say we have two top-level nodes: /user and /posts. The /posts node contains a list of all posts while /user/posts contains a list of all posts created by that user.

This is called denormalization and is a common practice to optimize your reads. Before today, in order to add a new post and assign it to the user, two writes were required: one to create the new post and one to assign the post to the user. This made code unnecessarily complex and led to verbose error handling and rollback logic to handle the case where the first write succeeds but the second write fails.

With today’s release, you can now do this in a single atomic update to both locations:

JavaScript

var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
// Generate a new push ID for the new post
var newPostRef = ref.child("posts").push();
var newPostKey = newPostRef.key();
// Create the data we want to update
var updatedUserData = {};
updatedUserData["user/posts/" + newPostKey] = true;
updatedUserData["posts/" + newPostKey] = {
  title: "New Post",
  content: "Here is my new post!"
};
// Do a deep-path update
ref.update(updatedUserData, function(error) {
  if (error) {
    console.log("Error updating data:", error);
  }
});

Java

Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
// Generate a new push ID for the new post
Firebase newPostRef = ref.child("posts").push();
String newPostKey = newPostRef.getKey();
// Create the data we want to update
Map newPost = new HashMap();
newPost.put("title", "New Post");
newPost.put("content", "Here is my new post!");
Map updatedUserData = new HashMap();
updatedUserData.put("users/posts/" + newPostKey, true);
updatedUserData.put("posts/" + newPostKey, newPost);
// Do a deep-path update
ref.updateChildren(updatedUserData, new Firebase.CompletionListener() {
   @Override
   public void onComplete(FirebaseError firebaseError, Firebase firebase) {
       if (firebaseError != null) {
           System.out.println("Error updating data: " + firebaseError.getMessage());
       }
   }
});  

Objective-C

Firebase *ref = [[Firebase alloc] initWithUrl: @"https://<YOUR-FIREBASE-APP>.firebaseio.com"];
// Generate a new push ID for the new post
Firebase *newPostRef = [[ref childByAppendingPath:@"posts"] childByAutoId];
NSString *newPostKey = newPostRef.key;
NSString *updateUserPath = [NSString stringWithFormat:@"users/posts/%@", newPostKey];
NSString *updatePostPath = [NSString stringWithFormat:@"posts/%@", newPostKey];
// Create the data we want to update
NSMutableDictionary *updatedUserData = [[NSMutableDictionary alloc] init];
[updatedUserData setValue:[NSNumber numberWithBool:YES] forKey:updateUserPath];
[updatedUserData setValue:@{@"title": @"New Post", @"content": @"Here is my new post!"} forKey:updatePostPath];
// Do a deep-path update
[ref updateChildValues:updatedUserData withCompletionBlock:^(NSError *error, Firebase *ref) {
    if (error) {
        NSLog(@"Error updating data: %@", error.debugDescription);
    }
}];

Swift

let ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com")
// Generate a new push ID for the new post
let newPostRef = ref.childByAppendingPath("posts").childByAutoId()
let newPostKey = newPostRef.key
// Create the data we want to update
let updatedUserData = ["users/posts/(newPostKey)": true, "posts/(newPostKey)": ["title": "New Post", "content": "Here is my new post!"]]
// Do a deep-path update
ref.updateChildValues(updatedUserData, withCompletionBlock: { (error, ref) -> Void in
    if (error) {
        print("Error updating data: (error.description)")
    }
})

Deep path updates let you write cleaner code and easily denormalize data across multiple nodes in your Firebase database.

Ordering queries by deep paths

Up until now, we have been able to order queries by direct children. Here’s a snippet of the dinosaur data stored in our database of dinosaur facts:

JavaScript

{
  "lambeosaurus": {
    "height" : 2.1,
    "length" : 12.5,
    "weight": 5000
  },
  "stegosaurus": {
    "height" : 4,
    "length" : 9,
    "weight" : 2500
  }
}

To read all dinosaurs ordered by height, we can order nodes by a common child key:

JavaScript

var ref = new Firebase("https://dinosaur-facts.firebaseio.com/dinosaurs");
ref.orderByChild("height").on("child_added", function(snapshot) {
  var dinoName = snapshot.key();
  var dinoData = snapshot.val();
  console.log(dinoName + " was " + dinoData.height + " meters tall");
});

Java

Firebase ref = new Firebase("https://dinosaur-facts.firebaseio.com/dinosaurs");
Query queryRef = ref.orderByChild("height");
queryRef.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChild) {
        DinosaurFacts facts = snapshot.getValue(DinosaurFacts.class);
        System.out.println(snapshot.getKey() + " was " + facts.getHeight() + " meters tall");
    }
    // ....
});

Objective-C

Firebase *ref = [[Firebase alloc] initWithUrl:@"https://dinosaur-facts.firebaseio.com/dinosaurs"];
[[ref queryOrderedByChild:@"height"]
    observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {

    NSLog(@"%@ was %@ meters tall", snapshot.key, snapshot.value[@"height"]);
}];

Swift

let ref = Firebase(url:"https://dinosaur-facts.firebaseio.com/dinosaurs")
ref.queryOrderedByChild("height").observeEventType(.ChildAdded, withBlock: { snapshot in
    if let height = snapshot.value["height"] as? Double {
        println("(snapshot.key) was (height) meters tall")
    }
})

This works great, but sometimes we want to order by descendants further down in our data tree. Let’s say our dinosaur data actually looked like this:

{
  "lambeosaurus": {
    "dimensions": {
      "height" : 2.1,
      "length" : 12.5,
      "weight": 5000
    }
  },
  "stegosaurus": {
    "dimensions": {
      "height" : 4,
      "length" : 9,
      "weight" : 2500
    }
  }
}

Previously there was no way to sort these dinosaurs based on any of their dimensions since they are stored too many levels deep. Now we can use a deep path query!

JavaScript

var ref = new Firebase("https://dinosaur-facts.firebaseio.com/dinosaurs");
ref.orderByChild("dimensions/height").on("child_added", function(snapshot) {
  var dinoName = snapshot.key();
  var dinoData = snapshot.val();
  console.log(dinoName + " was " + dinoData.dimensions.height + " meters tall");
});

Java

Firebase ref = new Firebase("https://dinosaur-facts.firebaseio.com/dinosaurs");
Query queryRef = ref.orderByChild("dimensions/height");
queryRef.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChild) {
        DinosaurFacts facts = snapshot.getValue(DinosaurFacts.class);
        System.out.println(snapshot.getKey() + " was " + facts.getHeight() + " meters tall");
    }
    // ....
});

Objective-C

Firebase *ref = [[Firebase alloc] initWithUrl:@"https://dinosaur-facts.firebaseio.com/dinosaurs"];
[[ref queryOrderedByChild:@"dimensions/height"]
    observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {

    NSLog(@"%@ was %@ meters tall", snapshot.key, snapshot.value[@"dimensions"][@"height"]);
}];

Swift

let ref = Firebase(url:"https://dinosaur-facts.firebaseio.com/dinosaurs")
ref.queryOrderedByChild("dimensions/height").observeEventType(.ChildAdded, withBlock: { snapshot in
    if let height = snapshot.childSnapshotForPath("dimensions/height").value as? Double {
        println("(snapshot.key) was (height) meters tall")
    }
})

The ability to order by deep paths gives you much more freedom in structuring your data.

Bitcode support for iOS

The Firebase iOS client is now compiled with bitcode support, a new iOS 9 feature. Bitcode is an “intermediate” language that Apple uses to represent your iOS and watchOS apps. According to Apple, “including bitcode will allow [us] to re-optimize your app binary in the future without the need to submit a new version of your app to the store.” This enables you to ship smaller apps through app thinning, as well as design for future products, decreasing development time and increasing the number of platforms upon which your app runs.

Bug fixes galore

On top of the new features mentioned above, we have also fixed a handful of bugs across each of the clients. Memory leaks, cache inconsistencies, and crashes have been tracked down and fixed in today’s releases.

Hack away!

All of these new features and bug fixes are now available in version 2.4.0 of the mobile clients and version 2.3.0 of the JavaScript client. See the installation and setup guides for each platform (Android, iOS, web) to learn how to get started with the latest clients.

We’d love to hear what you think of this release. Post any feedback or submit bug reports in our Google Group. Lastly, if you’ve built anything, don’t be shy! Share your apps with us on Twitter @Firebase. We’re excited to see what you build.