SQL transactions

There are several advantages into using prepared statements and transactions in SQL, in my case mostly with MariaDB. I will explain some of the advantages, some of the caveats and handy tips to manage them, specifically with Go.

Why SQL Transactions ?

We want our unit of work to be reliable and consistent, even in case of system failure.

We want to provide isolation between programs that access the database concurrently.

When you start an explicit transaction and issue a DML (Data Manipulation Language) statement, the resources being locked by the statement remain locked, and the results of statement are not visible from outside the transaction until you manually commit or rollback it.

This is what you may or may not need.

The InnoDB storage engine, which is my default go-to database storage engine and the one you should be going to for relational data, supports ACID-compliant transactions. SQL transactions are also a great way to avoid data races in the database engines, and to run queries against a cluster of databases, while preserving data consistency and integrity, a performant way to do batch operations, a way to avoid SQL injections into the database, and more. ACID properties

In order to achieve these 2 goals, a database transaction must satisfy the ACID properties, where:

How to run a SQL DB transaction from a SQL shell or Navicat / DataGrip?

It's pretty simple:

Using Transactions with Go

There are several reasons why you should prefer using the COMMIT, ROLLBACK and others in programmatic and type safe Go.

This gives you the opportunity to create prepared statements, execute them in a loop and then commit. If you were to not use transactions, and had to do 1000 INSERT statements, you would need 1000 database connections opening and closing one after the other, for single EXEC queries, while with prepared statements you would leave an open tunnel for communication from your application and the database. This obviously is much faster and brings less load on all services.

We always recommend you do prepared statements.

We always recommend you use prepared statements in a SQL transaction when executing more than one INSERT / UPDATE / DELETE statement to the database.

Rules of thumb

Real life transaction in Go

Here you can see some production transactions that I have slightly altered for this purpose.

// AddressBulkInsertion will perform a transaction to insert addresses in bulk func AddressBulkInsertion(db *sql.DB, addresses []Address) error{ tx, err := db.Begin() if err != nil { _ = tx.Rollback() return err } // here should be your SQL, never mind the naming here insertStmt, err := tx.Prepare(` INSERT INTO addresses (uid, address_data, created_at, updated_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE address_data = ?, updated_at = ?; `) if err != nil { _ = tx.Rollback() return err } defer insertStmt.Close() for i := range payload { now := time.Now().Unix() if _, errr := insertStmt.Exec( uuid.NewString(), address.addressData, now, now, ); errr != nil { // you might be interested in continuing the loop // even when some error occurs, perhaps due to // bad data or inconsistencies and you try to do as good as possible log.Println(errr.Error()) continue // otherwise simply rollback and return // if you want the entire bulk job to stop _ = tx.Rollback() return errr } } return tx.Commit() }

Useful Resouces

More information about transactions specifically for MariaDB.

More information about transactions

Official SQL package for working with Go