1

Here's my data structure, how it is organized (firebase database):

 -database
  -productsList
    -item1: 0
    -item2: 0
    -item3: 0
    -item4: 0
  -orders
    -order1:
      -...
    -order2:
      -...
    -...

I'm building a shopping app that has a list of products. Customers can choose what they want to buy and make an order. When they do the product's quantities would increase based on what they've chosen. Say a customer chose to buy 2 units of item2 e 5 units of item4, when one sends a request it will write at a different location with the following data like so:

-orders
 -orderKey
  -items
   -item2: 2
   -item4: 5

Then, the list would update to (based on onCreate method):

-productsList
 -item1: 0
 -item2: 2
 -item3: 0
 -item4: 5

But, let's suppose another customer did a request at the same time, the product list must acknowledge that.

I'm well aware of the firebase transactions, and this must be implemented as a transaction rather than an update. However, all the examples that a thoroughly searched here only show how to update a single node and not multiple children.

I'm using cloud functions to accomplish that, that means I'm using typescript.

What would be the best approach to update multiple children using a transaction?

How would be possible to only get and increment only the desirable nodes?

Please, any help would be much appreciated.


EDIT: After Frank van Puffelen answer my question I decided to make it more clear to explain exactly what I need to do.

I wish only to update one node named productsList and, in this example, its child nodes item2 and child4.

Here's some code to illustrate it better:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase)

export const onOrderCreate = functions.database
.ref('/orders/{oid}')
.onCreate(async (snapshot, context) => {
    const orderId = context.params.oid
    console.log(`new order made: ${oid}`)

    const orderData = snapshot.val()
    const items = getProductsAndQuantities(orderData)
    const path = 'productsList'

    return admin.database().ref(path).transaction(items => {
        // I would like to know if there's a way to update only the bought items, like
        const updatedList = {
            'item2': item2oldValue + 2,
            'item4': item2oldValue + 5 
        }
        return updatedList
    })

})

// Get the order data and return only the items and its quantities
function getProductsAndQuantities(data: any): Object {
    // Return items, exemple
    const products = {
        'item2': 2,
        'item4': 5 
    }

    return products
}
becktobeck
  • 11
  • 2

1 Answers1

3

Transactions run on a single node in your JSON tree, but they can run on any node in that tree.

This means that if you need to transactionally update multiple nodes, you will need to run a transaction at a common ancestor of those nodes. So if you need to update data under both orders and productsList, you'd need to run a transaction at the common ancestor of those.

Just keep in mind that:

  • A transaction reads the value at the location first, and returns it to your client. This means that the higher in the tree your transaction runs, the more bandwidth you'll use.
  • Transactions are automatically retried if another client changed the data between the read and the write. Doing this on a higher-level location, makes it more likely that the transaction is contended, i.e. multiple clients are updating data under that location at the same time.

One alternative is to use a multi-location update. Unlike a transaction, a multi-location update doesn't read the value of the entire common ancestor, and doesn't auto-retry.

Some disadvantages/things to consider in this approach:

  • If the new values depends on the existing value, you will still have to read the values yourself. But the advantage over a transaction is that you can read the precise values that you want to update, instead of reading the entire common ancestor.
  • Multi-location updates, are just updates, which means they have no mechanism to detect conflicting updates. This is something you can implement in security rules, but it's definitely far from trivial.
  • Multi-location updates don't auto-retry, so if the security rules reject the update (for example: if another client has just updates the same values), you will have to handle that in the client-side code yourself.

For some more on this approach, see my answer here: Is the way the Firebase database quickstart handles counts secure?


The final alternative I can think of is to run this code in a trusted environment where you control parallelism. Say for example you run it in a regular node process, then you can ensure there is only a single instance of the process running. And if you then also ensure that only this (administrative) process can update those values, you don't need a transaction for the update, and things will scale much better.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you very much for your answer. I think I haven't made it very clear exactly what I was trying to accomplish. I will edit my question and explain a little bit further. However, I only want to update one node *productsList* and its specific childer *item2* and *item4* for this particular example. Please, see my edited question. – becktobeck Mar 10 '19 at 11:02
  • In that case you need to run a transaction on the entire `productsList`. The good news is that it reduces the scope of the transaction, but the basic options I outlined above remain the same. – Frank van Puffelen Mar 10 '19 at 15:46