Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
database/sql: add examples for opening and testing a DB pool
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>
- Loading branch information
Showing
3 changed files
with
298 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
Oops, something went wrong.