]> Cypherpunks repositories - gostls13.git/commitdiff
database/sql: add examples for opening and testing a DB pool
authorDaniel Theophanes <kardianos@gmail.com>
Fri, 16 Mar 2018 17:57:44 +0000 (10:57 -0700)
committerDaniel Theophanes <kardianos@gmail.com>
Fri, 16 Nov 2018 17:17:09 +0000 (17:17 +0000)
Show two larger application examples. One example that
could be used in a CLI, the other in a long running
service. These demonstarates different strategies for
handling DB.Ping errors in context.

Fixes #23738

Change-Id: Id01213caf1f47917239a7506b01d30e37db74d31
Reviewed-on: https://go-review.googlesource.com/c/101216
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
src/database/sql/example_cli_test.go [new file with mode: 0644]
src/database/sql/example_service_test.go [new file with mode: 0644]
src/database/sql/example_test.go

diff --git a/src/database/sql/example_cli_test.go b/src/database/sql/example_cli_test.go
new file mode 100644 (file)
index 0000000..8c61d75
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sql_test
+
+import (
+       "context"
+       "database/sql"
+       "flag"
+       "log"
+       "os"
+       "os/signal"
+       "time"
+)
+
+var pool *sql.DB // Database connection pool.
+
+func Example_openDBCLI() {
+       id := flag.Int64("id", 0, "person ID to find")
+       dsn := flag.String("dsn", os.Getenv("DSN"), "connection data source name")
+       flag.Parse()
+
+       if len(*dsn) == 0 {
+               log.Fatal("missing dsn flag")
+       }
+       if *id == 0 {
+               log.Fatal("missing person ID")
+       }
+       var err error
+
+       // Opening a driver typically will not attempt to connect to the database.
+       pool, err = sql.Open("driver-name", *dsn)
+       if err != nil {
+               // This will not be a connection error, but a DSN parse error or
+               // another initialization error.
+               log.Fatal("unable to use data source name", err)
+       }
+       defer pool.Close()
+
+       pool.SetConnMaxLifetime(0)
+       pool.SetMaxIdleConns(3)
+       pool.SetMaxOpenConns(3)
+
+       ctx, stop := context.WithCancel(context.Background())
+       defer stop()
+
+       appSignal := make(chan os.Signal, 3)
+       signal.Notify(appSignal, os.Interrupt)
+
+       go func() {
+               select {
+               case <-appSignal:
+                       stop()
+               }
+       }()
+
+       Ping(ctx)
+
+       Query(ctx, *id)
+}
+
+// Ping the database to verify DSN provided by the user is valid and the
+// server accessible. If the ping fails exit the program with an error.
+func Ping(ctx context.Context) {
+       ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
+       defer cancel()
+
+       if err := pool.PingContext(ctx); err != nil {
+               log.Fatalf("unable to connect to database: %v", err)
+       }
+}
+
+// Query the database for the information requested and prints the results.
+// If the query fails exit the program with an error.
+func Query(ctx context.Context, id int64) {
+       ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+       defer cancel()
+
+       var name string
+       err := pool.QueryRowContext(ctx, "select p.name from people as p where p.id = :id;", sql.Named("id", id)).Scan(&name)
+       if err != nil {
+               log.Fatal("unable to execute search query", err)
+       }
+       log.Println("name=", name)
+}
diff --git a/src/database/sql/example_service_test.go b/src/database/sql/example_service_test.go
new file mode 100644 (file)
index 0000000..768307c
--- /dev/null
@@ -0,0 +1,158 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sql_test
+
+import (
+       "context"
+       "database/sql"
+       "encoding/json"
+       "fmt"
+       "io"
+       "log"
+       "net/http"
+       "time"
+)
+
+func Example_openDBService() {
+       // Opening a driver typically will not attempt to connect to the database.
+       db, err := sql.Open("driver-name", "database=test1")
+       if err != nil {
+               // This will not be a connection error, but a DSN parse error or
+               // another initialization error.
+               log.Fatal(err)
+       }
+       db.SetConnMaxLifetime(0)
+       db.SetMaxIdleConns(50)
+       db.SetMaxOpenConns(50)
+
+       s := &Service{db: db}
+
+       http.ListenAndServe(":8080", s)
+}
+
+type Service struct {
+       db *sql.DB
+}
+
+func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       db := s.db
+       switch r.URL.Path {
+       default:
+               http.Error(w, "not found", http.StatusNotFound)
+               return
+       case "/healthz":
+               ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
+               defer cancel()
+
+               err := s.db.PingContext(ctx)
+               if err != nil {
+                       http.Error(w, fmt.Sprintf("db down: %v", err), http.StatusFailedDependency)
+                       return
+               }
+               w.WriteHeader(http.StatusOK)
+               return
+       case "/quick-action":
+               // This is a short SELECT. Use the request context as the base of
+               // the context timeout.
+               ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
+               defer cancel()
+
+               id := 5
+               org := 10
+               var name string
+               err := db.QueryRowContext(ctx, `
+select
+       p.name
+from
+       people as p
+       join organization as o on p.organization = o.id
+where
+       p.id = :id
+       and o.id = :org
+;`,
+                       sql.Named("id", id),
+                       sql.Named("org", org),
+               ).Scan(&name)
+               if err != nil {
+                       if err == sql.ErrNoRows {
+                               http.Error(w, "not found", http.StatusNotFound)
+                               return
+                       }
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               io.WriteString(w, name)
+               return
+       case "/long-action":
+               // This is a long SELECT. Use the request context as the base of
+               // the context timeout, but give it some time to finish. If
+               // the client cancels before the query is done the query will also
+               // be canceled.
+               ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
+               defer cancel()
+
+               var names []string
+               rows, err := db.QueryContext(ctx, "select p.name from people as p where p.active = true;")
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+
+               for rows.Next() {
+                       var name string
+                       err = rows.Scan(&name)
+                       if err != nil {
+                               break
+                       }
+                       names = append(names, name)
+               }
+               // Check for errors during rows "Close".
+               // This may be more important if multiple statements are executed
+               // in a single batch and rows were written as well as read.
+               if closeErr := rows.Close(); closeErr != nil {
+                       http.Error(w, closeErr.Error(), http.StatusInternalServerError)
+                       return
+               }
+
+               // Check for row scan error.
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+
+               // Check for errors during row iteration.
+               if err = rows.Err(); err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+
+               json.NewEncoder(w).Encode(names)
+               return
+       case "/async-action":
+               // This action has side effects that we want to preserve
+               // even if the client cancels the HTTP request part way through.
+               // For this we do not use the http request context as a base for
+               // the timeout.
+               ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+               defer cancel()
+
+               var orderRef = "ABC123"
+               tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
+               _, err = tx.ExecContext(ctx, "stored_proc_name", orderRef)
+
+               if err != nil {
+                       tx.Rollback()
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               err = tx.Commit()
+               if err != nil {
+                       http.Error(w, "action in unknown state, check state before attempting again", http.StatusInternalServerError)
+                       return
+               }
+               w.WriteHeader(http.StatusOK)
+               return
+       }
+}
index da938b071a121df8771cb2dddc9e8c706b12dbda..6f9bd91276edfcf519194f9396163e13a2816d76 100644 (file)
@@ -13,8 +13,10 @@ import (
        "time"
 )
 
-var ctx = context.Background()
-var db *sql.DB
+var (
+       ctx context.Context
+       db  *sql.DB
+)
 
 func ExampleDB_QueryContext() {
        age := 27
@@ -24,13 +26,25 @@ func ExampleDB_QueryContext() {
        }
        defer rows.Close()
        names := make([]string, 0)
+
        for rows.Next() {
                var name string
                if err := rows.Scan(&name); err != nil {
+                       // Check for a scan error.
+                       // Query rows will be closed with defer.
                        log.Fatal(err)
                }
                names = append(names, name)
        }
+       // If the database is being written to ensure to check for Close
+       // errors that may be returned from the driver. The query may
+       // encounter an auto-commit error and be forced to rollback changes.
+       rerr := rows.Close()
+       if rerr != nil {
+               log.Fatal(err)
+       }
+
+       // Rows.Err will report the last error encountered by Rows.Scan.
        if err := rows.Err(); err != nil {
                log.Fatal(err)
        }
@@ -44,11 +58,11 @@ func ExampleDB_QueryRowContext() {
        err := db.QueryRowContext(ctx, "SELECT username, created_at FROM users WHERE id=?", id).Scan(&username, &created)
        switch {
        case err == sql.ErrNoRows:
-               log.Printf("No user with id %d", id)
+               log.Printf("no user with id %d\n", id)
        case err != nil:
-               log.Fatal(err)
+               log.Fatalf("query error: %v\n", err)
        default:
-               fmt.Printf("Username is %s, account created on %s\n", username, created)
+               log.Printf("username is %q, account created on %s\n", username, created)
        }
 }
 
@@ -63,7 +77,7 @@ func ExampleDB_ExecContext() {
                log.Fatal(err)
        }
        if rows != 1 {
-               panic(err)
+               log.Fatalf("expected to affect 1 row, affected %d", rows)
        }
 }
 
@@ -104,10 +118,10 @@ from
                if err := rows.Scan(&id, &name); err != nil {
                        log.Fatal(err)
                }
-               fmt.Printf("id %d name is %s\n", id, name)
+               log.Printf("id %d name is %s\n", id, name)
        }
        if !rows.NextResultSet() {
-               log.Fatal("expected more result sets", rows.Err())
+               log.Fatalf("expected more result sets: %v", rows.Err())
        }
        var roleMap = map[int64]string{
                1: "user",
@@ -122,7 +136,7 @@ from
                if err := rows.Scan(&id, &role); err != nil {
                        log.Fatal(err)
                }
-               fmt.Printf("id %d has role %s\n", id, roleMap[role])
+               log.Printf("id %d has role %s\n", id, roleMap[role])
        }
        if err := rows.Err(); err != nil {
                log.Fatal(err)
@@ -130,11 +144,23 @@ from
 }
 
 func ExampleDB_PingContext() {
+       // Ping and PingContext may be used to determine if communication with
+       // the database server is still possible.
+       //
+       // When used in a command line application Ping may be used to establish
+       // that further queries are possible; that the provided DSN is valid.
+       //
+       // When used in long running service Ping may be part of the health
+       // checking system.
+
        ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
        defer cancel()
+
+       status := "up"
        if err := db.PingContext(ctx); err != nil {
-               log.Fatal(err)
+               status = "down"
        }
+       log.Println(status)
 }
 
 func ExampleConn_BeginTx() {
@@ -162,7 +188,7 @@ func ExampleConn_ExecContext() {
        }
        defer conn.Close() // Return the connection to the pool.
        id := 41
-       result, err := conn.ExecContext(ctx, `UPDATE balances SET balance = balance + 10 WHERE user_id = ?`, id)
+       result, err := conn.ExecContext(ctx, `UPDATE balances SET balance = balance + 10 WHERE user_id = ?;`, id)
        if err != nil {
                log.Fatal(err)
        }
@@ -171,7 +197,7 @@ func ExampleConn_ExecContext() {
                log.Fatal(err)
        }
        if rows != 1 {
-               panic(err)
+               log.Fatalf("expected single row affected, got %d rows affected", rows)
        }
 }
 
@@ -184,9 +210,9 @@ func ExampleTx_ExecContext() {
        _, execErr := tx.ExecContext(ctx, "UPDATE users SET status = ? WHERE id = ?", "paid", id)
        if execErr != nil {
                if rollbackErr := tx.Rollback(); rollbackErr != nil {
-                       log.Printf("Could not roll back: %v\n", rollbackErr)
+                       log.Fatalf("update failed: %v, unable to rollback: %v\n", execErr, rollbackErr)
                }
-               log.Fatal(execErr)
+               log.Fatalf("update failed: %v", execErr)
        }
        if err := tx.Commit(); err != nil {
                log.Fatal(err)
@@ -199,17 +225,17 @@ func ExampleTx_Rollback() {
                log.Fatal(err)
        }
        id := 53
-       _, err = tx.ExecContext(ctx, "UPDATE drivers SET status = ? WHERE id = ?", "assigned", id)
+       _, err = tx.ExecContext(ctx, "UPDATE drivers SET status = ? WHERE id = ?;", "assigned", id)
        if err != nil {
                if rollbackErr := tx.Rollback(); rollbackErr != nil {
-                       log.Printf("Could not roll back: %v\n", rollbackErr)
+                       log.Fatalf("update drivers: unable to rollback: %v", rollbackErr)
                }
                log.Fatal(err)
        }
-       _, err = tx.ExecContext(ctx, "UPDATE pickups SET driver_id = $1", id)
+       _, err = tx.ExecContext(ctx, "UPDATE pickups SET driver_id = $1;", id)
        if err != nil {
                if rollbackErr := tx.Rollback(); rollbackErr != nil {
-                       log.Printf("Could not roll back: %v\n", rollbackErr)
+                       log.Fatalf("update failed: %v, unable to back: %v", err, rollbackErr)
                }
                log.Fatal(err)
        }
@@ -225,17 +251,18 @@ func ExampleStmt() {
                log.Fatal(err)
        }
        defer stmt.Close()
+
        // Then reuse it each time you need to issue the query.
        id := 43
        var username string
        err = stmt.QueryRowContext(ctx, id).Scan(&username)
        switch {
        case err == sql.ErrNoRows:
-               log.Printf("No user with that ID.")
+               log.Fatalf("no user with id %d", id)
        case err != nil:
                log.Fatal(err)
        default:
-               fmt.Printf("Username is %s\n", username)
+               log.Printf("username is %s\n", username)
        }
 }
 
@@ -245,17 +272,19 @@ func ExampleStmt_QueryRowContext() {
        if err != nil {
                log.Fatal(err)
        }
+       defer stmt.Close()
+
        // Then reuse it each time you need to issue the query.
        id := 43
        var username string
        err = stmt.QueryRowContext(ctx, id).Scan(&username)
        switch {
        case err == sql.ErrNoRows:
-               log.Printf("No user with that ID.")
+               log.Fatalf("no user with id %d", id)
        case err != nil:
                log.Fatal(err)
        default:
-               fmt.Printf("Username is %s\n", username)
+               log.Printf("username is %s\n", username)
        }
 }
 
@@ -266,6 +295,7 @@ func ExampleRows() {
                log.Fatal(err)
        }
        defer rows.Close()
+
        names := make([]string, 0)
        for rows.Next() {
                var name string
@@ -274,8 +304,9 @@ func ExampleRows() {
                }
                names = append(names, name)
        }
+       // Check for errors from iterating over rows.
        if err := rows.Err(); err != nil {
                log.Fatal(err)
        }
-       fmt.Printf("%s are %d years old", strings.Join(names, ", "), age)
+       log.Printf("%s are %d years old", strings.Join(names, ", "), age)
 }