Before you begin
- .
- Start up a or local cluster.
- Choose the instructions that correspond to whether your cluster is secure or insecure:
- Secure
- Insecure
Step 1. Create the maxroach user and bank database
Start the :$ cockroach sql --certs-dir=certs
maxroach user and bank database:> CREATE USER IF NOT EXISTS maxroach;
> CREATE DATABASE bank;
maxroach user the necessary permissions:> GRANT ALL ON DATABASE bank TO maxroach;
> \q
Step 2. Generate a certificate for the maxroach user
Create a certificate and key for the maxroach user by running the following command:$ cockroach cert create-client maxroach --certs-dir=certs --ca-key=my-safe-directory/ca.key
maxroach as the user.Step 3. Run the Go code
The sample code shown below uses upper/db to map Go-specific objects to SQL operations. Specifically, the code:- Creates the
accountstable, if it does not already exist. - Deletes any existing rows in the
accountstable. - Inserts two rows into the
accountstable. - Prints the rows in the
accountstable to the terminal. - Deletes the first row in the
accountstable. - Updates the rows in the
accountstable within an explicit . - Prints the rows in the
accountstable to the terminal once more.
package main
"fmt"
"log"
"time"
"github.com/upper/db/v4"
"github.com/upper/db/v4/adapter/cockroachdb"
)
// The settings variable stores connection details.
var settings = cockroachdb.ConnectionURL{
Host: "localhost",
Database: "bank",
User: "maxroach",
Options: map[string]string{
// Secure node.
"sslrootcert": "certs/ca.crt",
"sslkey": "certs/client.maxroach.key",
"sslcert": "certs/client.maxroach.crt",
},
}
// Accounts is a handy way to represent a collection.
func Accounts(sess db.Session) db.Store {
return sess.Collection("accounts")
}
// Account is used to represent a single record in the "accounts" table.
type Account struct {
ID uint64 `db:"id,omitempty"`
Balance int64 `db:"balance"`
}
// Collection is required in order to create a relation between the Account
// struct and the "accounts" table.
func (a *Account) Store(sess db.Session) db.Store {
return Accounts(sess)
}
// createTables creates all the tables that are neccessary to run this example.
func createTables(sess db.Session) error {
_, err := sess.SQL().Exec(`
CREATE TABLE IF NOT EXISTS accounts (
ID SERIAL PRIMARY KEY,
balance INT
)
`)
if err != nil {
return err
}
return nil
}
// crdbForceRetry can be used to simulate a transaction error and
// demonstrate upper/db's ability to retry the transaction automatically.
//
// By default, upper/db will retry the transaction five times, if you want
// to modify this number use: sess.SetMaxTransactionRetries(n).
//
// This is only used for demonstration purposes and not intended
// for production code.
func crdbForceRetry(sess db.Session) error {
var err error
// The first statement in a transaction can be retried transparently on the
// server, so we need to add a placeholder statement so that our
// force_retry() statement isn't the first one.
_, err = sess.SQL().Exec(`SELECT 1`)
if err != nil {
return err
}
// If force_retry is called during the specified interval from the beginning
// of the transaction it returns a retryable error. If not, 0 is returned
// instead of an error.
_, err = sess.SQL().Exec(`SELECT crdb_internal.force_retry('1s'::INTERVAL)`)
if err != nil {
return err
}
return nil
}
func main() {
// Connect to the local CockroachDB node.
sess, err := cockroachdb.Open(settings)
if err != nil {
log.Fatal("cockroachdb.Open: ", err)
}
defer sess.Close()
// Adjust this number to fit your specific needs (set to 5, by default)
// sess.SetMaxTransactionRetries(10)
// Create the "accounts" table
createTables(sess)
// Delete all the previous items in the "accounts" table.
err = Accounts(sess).Truncate()
if err != nil {
log.Fatal("Truncate: ", err)
}
// Create a new account with a balance of 1000.
account1 := Account{Balance: 1000}
err = Accounts(sess).InsertReturning(&account1)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Create a new account with a balance of 250.
account2 := Account{Balance: 250}
err = Accounts(sess).InsertReturning(&account2)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Printing records
printRecords(sess)
// Change the balance of the first account.
account1.Balance = 500
err = sess.Save(&account1)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Change the balance of the second account.
account2.Balance = 999
err = sess.Save(&account2)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Printing records
printRecords(sess)
// Delete the first record.
err = sess.Delete(&account1)
if err != nil {
log.Fatal("Delete: ", err)
}
startTime := time.Now()
// Add a couple of new records within a transaction.
err = sess.Tx(func(tx db.Session) error {
var err error
if err = tx.Save(&Account{Balance: 887}); err != nil {
return err
}
if time.Now().Sub(startTime) < time.Second*1 {
// Will fail continuously for 2 seconds.
if err = crdbForceRetry(tx); err != nil {
return err
}
}
if err = tx.Save(&Account{Balance: 342}); err != nil {
return err
}
return nil
})
if err != nil {
log.Fatal("Could not commit transaction: ", err)
}
// Printing records
printRecords(sess)
}
func printRecords(sess db.Session) {
accounts := []Account{}
err := Accounts(sess).Find().All(&accounts)
if err != nil {
log.Fatal("Find: ", err)
}
log.Printf("Balances:")
for i := range accounts {
fmt.Printf("\taccounts[%d]: %d\n", accounts[i].ID, accounts[i].Balance)
}
}
crdbForceRetry()). Upper/db’s CockroachDB adapter when transaction errors are thrown. As a result, this function forces a transaction retry.To run the code, copy the sample above, or download it directly.To clone a version of the code below that connects to insecure clusters, run the following command:
git clone https://github.com/cockroachlabs/hello-world-go-upperdb/Note that you will need to edit the connection string to use the certificates that you generated when you set up your secure cluster.The
--insecure flag used in this tutorial is intended for non-production testing only. To run CockroachDB in production, use a secure cluster instead.Step 1. Create the maxroach user and bank database
Start the :$ cockroach sql --insecure
maxroach user and bank database:> CREATE USER IF NOT EXISTS maxroach;
> CREATE DATABASE bank;
maxroach user the necessary permissions:> GRANT ALL ON DATABASE bank TO maxroach;
> \q
Step 2. Run the Go code
The sample code shown below uses upper/db to map Go-specific objects to SQL operations. Specifically, the code:- Creates the
accountstable, if it does not already exist. - Deletes any existing rows in the
accountstable. - Inserts two rows into the
accountstable. - Prints the rows in the
accountstable to the terminal. - Deletes the first row in the
accountstable. - Updates the rows in the
accountstable within an explicit . - Prints the rows in the
accountstable to the terminal once more.
package main
"fmt"
"log"
"time"
"github.com/upper/db/v4"
"github.com/upper/db/v4/adapter/cockroachdb"
)
// The settings variable stores connection details.
var settings = cockroachdb.ConnectionURL{
Host: "localhost",
Database: "bank",
User: "maxroach",
Options: map[string]string{
// Insecure node.
"sslmode": "disable",
},
}
// Accounts is a handy way to represent a collection.
func Accounts(sess db.Session) db.Store {
return sess.Collection("accounts")
}
// Account is used to represent a single record in the "accounts" table.
type Account struct {
ID uint64 `db:"id,omitempty"`
Balance int64 `db:"balance"`
}
// Collection is required in order to create a relation between the Account
// struct and the "accounts" table.
func (a *Account) Store(sess db.Session) db.Store {
return Accounts(sess)
}
// createTables creates all the tables that are neccessary to run this example.
func createTables(sess db.Session) error {
_, err := sess.SQL().Exec(`
CREATE TABLE IF NOT EXISTS accounts (
ID SERIAL PRIMARY KEY,
balance INT
)
`)
if err != nil {
return err
}
return nil
}
// crdbForceRetry can be used to simulate a transaction error and
// demonstrate upper/db's ability to retry the transaction automatically.
//
// By default, upper/db will retry the transaction five times, if you want
// to modify this number use: sess.SetMaxTransactionRetries(n).
//
// This is only used for demonstration purposes and not intended
// for production code.
func crdbForceRetry(sess db.Session) error {
var err error
// The first statement in a transaction can be retried transparently on the
// server, so we need to add a placeholder statement so that our
// force_retry() statement isn't the first one.
_, err = sess.SQL().Exec(`SELECT 1`)
if err != nil {
return err
}
// If force_retry is called during the specified interval from the beginning
// of the transaction it returns a retryable error. If not, 0 is returned
// instead of an error.
_, err = sess.SQL().Exec(`SELECT crdb_internal.force_retry('1s'::INTERVAL)`)
if err != nil {
return err
}
return nil
}
func main() {
// Connect to the local CockroachDB node.
sess, err := cockroachdb.Open(settings)
if err != nil {
log.Fatal("cockroachdb.Open: ", err)
}
defer sess.Close()
// Adjust this number to fit your specific needs (set to 5, by default)
// sess.SetMaxTransactionRetries(10)
// Create the "accounts" table
createTables(sess)
// Delete all the previous items in the "accounts" table.
err = Accounts(sess).Truncate()
if err != nil {
log.Fatal("Truncate: ", err)
}
// Create a new account with a balance of 1000.
account1 := Account{Balance: 1000}
err = Accounts(sess).InsertReturning(&account1)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Create a new account with a balance of 250.
account2 := Account{Balance: 250}
err = Accounts(sess).InsertReturning(&account2)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Printing records
printRecords(sess)
// Change the balance of the first account.
account1.Balance = 500
err = sess.Save(&account1)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Change the balance of the second account.
account2.Balance = 999
err = sess.Save(&account2)
if err != nil {
log.Fatal("sess.Save: ", err)
}
// Printing records
printRecords(sess)
// Delete the first record.
err = sess.Delete(&account1)
if err != nil {
log.Fatal("Delete: ", err)
}
startTime := time.Now()
// Add a couple of new records within a transaction.
err = sess.Tx(func(tx db.Session) error {
var err error
if err = tx.Save(&Account{Balance: 887}); err != nil {
return err
}
if time.Now().Sub(startTime) < time.Second*1 {
// Will fail continuously for 2 seconds.
if err = crdbForceRetry(tx); err != nil {
return err
}
}
if err = tx.Save(&Account{Balance: 342}); err != nil {
return err
}
return nil
})
if err != nil {
log.Fatal("Could not commit transaction: ", err)
}
// Printing records
printRecords(sess)
}
func printRecords(sess db.Session) {
accounts := []Account{}
err := Accounts(sess).Find().All(&accounts)
if err != nil {
log.Fatal("Find: ", err)
}
log.Printf("Balances:")
for i := range accounts {
fmt.Printf("\taccounts[%d]: %d\n", accounts[i].ID, accounts[i].Balance)
}
}
crdbForceRetry()). Upper/db’s CockroachDB adapter when transaction errors are thrown. As a result, this function forces a transaction retry.Copy the code or download it directly.To clone a version of the code below that connects to insecure clusters, run the following command:git clone https://github.com/cockroachlabs/hello-world-go-upperdb/go mod init:
$ go mod init hello-world-go-upperdb
$ go run main.go
go: finding module for package github.com/upper/db/v4
go: finding module for package github.com/upper/db/v4/adapter/cockroachdb
go: found github.com/upper/db/v4 in github.com/upper/db/v4 v4.0.0
2020/09/16 10:31:55 Balances:
accounts[590467288222990337]: 1000
accounts[590467288229576705]: 250
2020/09/16 10:31:55 Balances:
accounts[590467288222990337]: 500
accounts[590467288229576705]: 999
2020/09/16 10:31:55 upper/db: log_level=WARNING file=go/pkg/mod/github.com/upper/db/v4@v4.0.0/internal/sqladapter/session.go:642
Session ID: 00006
Transaction ID: 00005
Query: SELECT crdb_internal.force_retry('1ms'::INTERVAL)
Error: pq: restart transaction: crdb_internal.force_retry(): TransactionRetryWithProtoRefreshError: forced by crdb_internal.force_retry()
Time taken: 0.00171s
Context: context.Background
2020/09/16 10:31:55 upper/db: log_level=WARNING file=go/pkg/mod/github.com/upper/db/v4@v4.0.0/internal/sqladapter/session.go:642
Session ID: 00006
Transaction ID: 00005
Query: INSERT INTO "accounts" ("balance") VALUES ($1) RETURNING "id"
Arguments: []interface {}{887}
Error: pq: current transaction is aborted, commands ignored until end of transaction block
Time taken: 0.00065s
Context: context.Background
2020/09/16 10:31:56 Balances:
accounts[590467288229576705]: 999
accounts[590467288342757377]: 887
accounts[590467288350064641]: 342

