Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this the proper way to write a multi-statement transaction with Neo4j?

I am having a hard time interpretting the documentation from Neo4j about transactions. Their documentation seems to indicate preference to doing it this way rather than explicitly declaring tx.commit() and tx.rollback().

Does this look best practice with respect to multi-statement transactions and neo4j-driver?

const register = async (container, user) => {
    const session = driver.session()
    const timestamp = Date.now()

    const saltRounds = 10
    const pwd = await utils.bcrypt.hash(user.password, saltRounds)

    try {
        //Start registration transaction
            const registerUser = session.writeTransaction(async (transaction) => {
            const initialCommit = await transaction
                .run(`
                    CREATE (p:Person {
                        email: '${user.email}',
                        tel: '${user.tel}',
                        pwd: '${pwd}',
                        created: '${timestamp}'
                    })
                    RETURN p AS Person
                `)

            const initialResult = initialCommit.records
                .map((x) => {
                    return {
                        id: x.get('Person').identity.low,
                        created: x.get('Person').properties.created
                    }
                })
                .shift()

            //Generate serial
            const data = `${initialResult.id}${initialResult.created}`
            const serial = crypto.sha256(data)

            const finalCommit = await transaction
                .run(`
                    MATCH (p:Person)
                    WHERE p.email = '${user.email}'
                    SET p.serialNumber = '${serial}'
                    RETURN p AS Person
                `)

            const finalResult = finalCommit.records
                .map((x) => {
                    return {
                        serialNumber: x.get('Person').properties.serialNumber,
                        email: x.get('Person').properties.email,
                        tel: x.get('Person').properties.tel
                    }
                })
                .shift()

            //Merge both results for complete person data
            return Object.assign({}, initialResult, finalResult)
        })

        //Commit or rollback transaction
        return registerUser
            .then((commit) => {
                session.close()
                return commit
            })
            .catch((rollback) => {
                console.log(`Transaction problem: ${JSON.stringify(rollback, null, 2)}`)
                throw [`reg1`]
            })
    } catch (error) {
    session.close()
        throw error
    }
}

Here is the reduced version of the logic:

const register = (user) => {
    const session = driver.session()
    const performTransaction = session.writeTransaction(async (tx) => {

        const statementOne = await tx.run(queryOne)
        const resultOne = statementOne.records.map((x) => x.get('node')).slice()

        // Do some work that uses data from statementOne

        const statementTwo = await tx.run(queryTwo)
        const resultTwo = statementTwo.records.map((x) => x.get('node')).slice()

        // Do final processing

        return finalResult
    })

    return performTransaction.then((commit) => {
           session.close()
           return commit
    }).catch((rollback) => {
            throw rollback
    })
}

Neo4j experts, is the above code the correct use of neo4j-driver ?

I would rather do this because its more linear and synchronous:

const register = (user) => {
    const session = driver.session()
    const tx = session.beginTransaction()

    const statementOne = await tx.run(queryOne)
    const resultOne = statementOne.records.map((x) => x.get('node')).slice()

    // Do some work that uses data from statementOne

    const statementTwo = await tx.run(queryTwo)
    const resultTwo = statementTwo.records.map((x) => x.get('node')).slice()

    // Do final processing
    const finalResult = { obj1, ...obj2 }
    let success = true

   if (success) {
       tx.commit()
       session.close()
       return finalResult
   } else {
       tx.rollback()
       session.close()
       return false
   }
}

I'm sorry for the long post, but I cannot find any references anywhere, so the community needs this data.

like image 659
agm1984 Avatar asked Aug 15 '17 18:08

agm1984


People also ask

Is Neo4j transactional?

In order to fully maintain data integrity and ensure good transactional behavior, Neo4j DBMS supports the ACID properties: Atomicity — If any part of a transaction fails, the database state is left unchanged. Consistency — Any transaction will leave the database in a consistent state.

How many databases can the client access in the single transaction Neo4j?

Neo4j offers the ability to work with multiple databases within the same DBMS. For Community Edition, this is limited to one user database, plus the system database. From a driver API perspective, sessions have a DBMS scope, and the default database for a session can be selected on session construction.


1 Answers

After much more work, this is the syntax we have settled on for multi-statement transactions:

  1. Start session
  2. Start transaction
  3. Use try/catch block after (to enable proper scope in catch block)
  4. Perform queries in the try block
  5. Rollback in the catch block

.

 const someQuery = async () => {
     const session = Neo4J.session()
     const tx = session.beginTransaction()
     try {
         const props = {
             one: 'Bob',
             two: 'Alice'
         }
         const tx1 = await tx
             .run(`
                 MATCH (n:Node)-[r:REL]-(o:Other)
                 WHERE n.one = $props.one
                 AND n.two = $props.two
                 RETURN n AS One, o AS Two
             `, { props })
             .then((result) => {
                 return {
                     data: '...'
                 }
             })
             .catch((err) => {
                 throw 'Problem in first query. ' + e
             })

         // Do some work using tx1
         const updatedProps = {
             _id: 3,
             four: 'excellent'
         }

         const tx2 = await tx
             .run(`
                 MATCH (n:Node)
                 WHERE id(n) = toInteger($updatedProps._id)
                 SET n.four = $updatedProps.four
                 RETURN n AS One, o AS Two
             `, { updatedProps })
             .then((result) => {
                 return {
                     data: '...'
                 }
             })
             .catch((err) => {
                 throw 'Problem in second query. ' + e
             })

         // Do some work using tx2
         if (problem) throw 'Rollback ASAP.'

         await tx.commit
         session.close()
         return Object.assign({}, tx1, { tx2 })
     } catch (e) {
         tx.rollback()
         session.close()
         throw 'someQuery# ' + e
     }
 }

I will just note that if you are passing numbers into Neo4j, you should wrap them inside the Cypher Query with toInteger() so that they are parsed correctly.

I included examples of query parameters also and how to use them. I found it cleans up the code a little.

Besides that, you basically can chain as many queries inside the transaction as you want, but keep in mind 2 things:

  1. Neo4j write-locks all involved nodes during a transaction, so if you have several processes all performing operations on the same node, you will see that only one process can complete a transaction at a time. We made our own business logic to handle write issues and opted to not even use transactions. It is working very well so far, writing 100,000 nodes and creating 100,000 relationships in about 30 seconds spread over 10 processes. It took 10 times longer to do in a transaction. We experience no deadlocking or race conditions using UNWIND.
  2. You have to await the tx.commit() or it won't commit before it nukes the session.

My opinion is that this type of transaction works great if you are using Polyglot (multiple databases) and need to create a node, and then write a document to MongoDB and then set the Mongo ID on the node.

It's very easy to reason about, and extend as needed.

like image 72
agm1984 Avatar answered Oct 10 '22 21:10

agm1984