mirror of https://github.com/jlelse/GoBlog
Refactor database hooks to make them more useful
This commit is contained in:
parent
2bf98dddaa
commit
da9ea7c0d7
101
database.go
101
database.go
|
@ -1,15 +1,14 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gchaincl/sqlhooks/v2"
|
|
||||||
sqlite "github.com/mattn/go-sqlite3"
|
sqlite "github.com/mattn/go-sqlite3"
|
||||||
"github.com/schollz/sqlite3dump"
|
"github.com/schollz/sqlite3dump"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
|
@ -22,10 +21,11 @@ type database struct {
|
||||||
sg singleflight.Group // singleflight group for prepared statements
|
sg singleflight.Group // singleflight group for prepared statements
|
||||||
ps sync.Map // map with prepared statements
|
ps sync.Map // map with prepared statements
|
||||||
// Other things
|
// Other things
|
||||||
pc singleflight.Group // persistant cache
|
pc singleflight.Group // persistant cache
|
||||||
pcm sync.Mutex // post creation
|
pcm sync.Mutex // post creation
|
||||||
sp singleflight.Group // singleflight group for short path requests
|
sp singleflight.Group // singleflight group for short path requests
|
||||||
spc sync.Map // shortpath cache
|
spc sync.Map // shortpath cache
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) initDatabase(logging bool) (err error) {
|
func (a *goBlog) initDatabase(logging bool) (err error) {
|
||||||
|
@ -60,8 +60,8 @@ func (a *goBlog) initDatabase(logging bool) (err error) {
|
||||||
|
|
||||||
func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
|
func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
|
||||||
// Register driver
|
// Register driver
|
||||||
dbDriverName := generateRandomString(15)
|
dbDriverName := "goblog_db_" + generateRandomString(15)
|
||||||
var dr driver.Driver = &sqlite.SQLiteDriver{
|
sql.Register(dbDriverName, &sqlite.SQLiteDriver{
|
||||||
ConnectHook: func(c *sqlite.SQLiteConn) error {
|
ConnectHook: func(c *sqlite.SQLiteConn) error {
|
||||||
// Register functions
|
// Register functions
|
||||||
for n, f := range map[string]interface{}{
|
for n, f := range map[string]interface{}{
|
||||||
|
@ -79,13 +79,9 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
if c := a.cfg.Db; c != nil && c.Debug {
|
|
||||||
dr = sqlhooks.Wrap(dr, &dbHooks{})
|
|
||||||
}
|
|
||||||
sql.Register("goblog_db_"+dbDriverName, dr)
|
|
||||||
// Open db
|
// Open db
|
||||||
db, err := sql.Open("goblog_db_"+dbDriverName, file+"?mode=rwc&_journal_mode=WAL&_busy_timeout=100&cache=shared")
|
db, err := sql.Open(dbDriverName, file+"?mode=rwc&_journal_mode=WAL&_busy_timeout=100&cache=shared")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -118,8 +114,14 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Debug
|
||||||
|
debug := false
|
||||||
|
if c := a.cfg.Db; c != nil && c.Debug {
|
||||||
|
debug = true
|
||||||
|
}
|
||||||
return &database{
|
return &database{
|
||||||
db: db,
|
db: db,
|
||||||
|
debug: debug,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +163,9 @@ func (db *database) prepare(query string) (*sql.Stmt, error) {
|
||||||
return st, nil
|
return st, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if db.debug {
|
||||||
|
log.Printf(`Failed to prepare query "%s": %s`, query, err.Error())
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return stmt.(*sql.Stmt), nil
|
return stmt.(*sql.Stmt), nil
|
||||||
|
@ -172,45 +177,69 @@ func (db *database) exec(query string, args ...interface{}) (sql.Result, error)
|
||||||
// Lock execution
|
// Lock execution
|
||||||
db.em.Lock()
|
db.em.Lock()
|
||||||
defer db.em.Unlock()
|
defer db.em.Unlock()
|
||||||
// Check if prepared cache should be skipped
|
// Check if no cache arg set
|
||||||
|
cache := true
|
||||||
if len(args) > 0 && args[0] == dbNoCache {
|
if len(args) > 0 && args[0] == dbNoCache {
|
||||||
return db.db.Exec(query, args[1:]...)
|
cache = false
|
||||||
|
args = args[1:]
|
||||||
}
|
}
|
||||||
// Use prepared statement
|
// Maybe prepare
|
||||||
st, _ := db.prepare(query)
|
var st *sql.Stmt
|
||||||
|
if cache {
|
||||||
|
st, _ = db.prepare(query)
|
||||||
|
}
|
||||||
|
// Prepare context, call hook
|
||||||
|
ctx := db.dbBefore(context.Background(), query, args...)
|
||||||
|
defer db.dbAfter(ctx, query, args...)
|
||||||
|
// Execute
|
||||||
if st != nil {
|
if st != nil {
|
||||||
return st.Exec(args...)
|
return st.ExecContext(ctx, args...)
|
||||||
}
|
}
|
||||||
// Or execute directly
|
return db.db.ExecContext(ctx, query, args...)
|
||||||
return db.db.Exec(query, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *database) query(query string, args ...interface{}) (*sql.Rows, error) {
|
func (db *database) query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||||
// Check if prepared cache should be skipped
|
// Check if no cache arg set
|
||||||
|
cache := true
|
||||||
if len(args) > 0 && args[0] == dbNoCache {
|
if len(args) > 0 && args[0] == dbNoCache {
|
||||||
return db.db.Query(query, args[1:]...)
|
cache = false
|
||||||
|
args = args[1:]
|
||||||
}
|
}
|
||||||
// Use prepared statement
|
// Maybe prepare
|
||||||
st, _ := db.prepare(query)
|
var st *sql.Stmt
|
||||||
|
if cache {
|
||||||
|
st, _ = db.prepare(query)
|
||||||
|
}
|
||||||
|
// Prepare context, call hook
|
||||||
|
ctx := db.dbBefore(context.Background(), query, args...)
|
||||||
|
defer db.dbAfter(ctx, query, args...)
|
||||||
|
// Query
|
||||||
if st != nil {
|
if st != nil {
|
||||||
return st.Query(args...)
|
return st.QueryContext(ctx, args...)
|
||||||
}
|
}
|
||||||
// Or query directly
|
return db.db.QueryContext(ctx, query, args...)
|
||||||
return db.db.Query(query, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *database) queryRow(query string, args ...interface{}) (*sql.Row, error) {
|
func (db *database) queryRow(query string, args ...interface{}) (*sql.Row, error) {
|
||||||
// Check if prepared cache should be skipped
|
// Check if no cache arg set
|
||||||
|
cache := true
|
||||||
if len(args) > 0 && args[0] == dbNoCache {
|
if len(args) > 0 && args[0] == dbNoCache {
|
||||||
return db.db.QueryRow(query, args[1:]...), nil
|
cache = false
|
||||||
|
args = args[1:]
|
||||||
}
|
}
|
||||||
// Use prepared statement
|
// Maybe prepare
|
||||||
st, _ := db.prepare(query)
|
var st *sql.Stmt
|
||||||
|
if cache {
|
||||||
|
st, _ = db.prepare(query)
|
||||||
|
}
|
||||||
|
// Prepare context, call hook
|
||||||
|
ctx := db.dbBefore(context.Background(), query, args...)
|
||||||
|
defer db.dbAfter(ctx, query, args...)
|
||||||
|
// Query
|
||||||
if st != nil {
|
if st != nil {
|
||||||
return st.QueryRow(args...), nil
|
return st.QueryRowContext(ctx, args...), nil
|
||||||
}
|
}
|
||||||
// Or query directly
|
return db.db.QueryRowContext(ctx, query, args...), nil
|
||||||
return db.db.QueryRow(query, args...), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other things
|
// Other things
|
||||||
|
|
|
@ -2,20 +2,62 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
|
||||||
|
|
||||||
type dbHooks struct{}
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
const dbHooksBegin contextKey = "begin"
|
const dbHooksBegin contextKey = "begin"
|
||||||
|
|
||||||
func (h *dbHooks) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
|
func (db *database) dbBefore(ctx context.Context, query string, args ...interface{}) context.Context {
|
||||||
return context.WithValue(ctx, dbHooksBegin, time.Now()), nil
|
if !db.debug {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, dbHooksBegin, time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *dbHooks) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
|
func (db *database) dbAfter(ctx context.Context, query string, args ...interface{}) {
|
||||||
begin := ctx.Value(dbHooksBegin).(time.Time)
|
if !db.debug {
|
||||||
log.Printf("SQL: %s %q (%s)\n", query, args, time.Since(begin))
|
return
|
||||||
return ctx, nil
|
}
|
||||||
|
dur := time.Since(ctx.Value(dbHooksBegin).(time.Time))
|
||||||
|
var logBuilder strings.Builder
|
||||||
|
logBuilder.WriteString("\nQuery: ")
|
||||||
|
logBuilder.WriteString(`"`)
|
||||||
|
logBuilder.WriteString(query)
|
||||||
|
logBuilder.WriteString(`"`)
|
||||||
|
if len(args) > 0 {
|
||||||
|
logBuilder.WriteString("\nArgs: ")
|
||||||
|
for i, arg := range args {
|
||||||
|
if i > 0 {
|
||||||
|
logBuilder.WriteString(", ")
|
||||||
|
}
|
||||||
|
if named, ok := arg.(sql.NamedArg); ok && named.Name != "" {
|
||||||
|
logBuilder.WriteString("(")
|
||||||
|
logBuilder.WriteString(named.Name)
|
||||||
|
logBuilder.WriteString(`) "`)
|
||||||
|
logBuilder.WriteString(argToString(named.Value))
|
||||||
|
logBuilder.WriteString(`"`)
|
||||||
|
} else {
|
||||||
|
logBuilder.WriteString(`"`)
|
||||||
|
logBuilder.WriteString(argToString(arg))
|
||||||
|
logBuilder.WriteString(`"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logBuilder.WriteString("\nDuration: ")
|
||||||
|
logBuilder.WriteString(dur.String())
|
||||||
|
log.Println(logBuilder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func argToString(arg interface{}) string {
|
||||||
|
val := cast.ToString(arg)
|
||||||
|
if val == "" {
|
||||||
|
val = fmt.Sprintf("%v", arg)
|
||||||
|
}
|
||||||
|
return val
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -16,7 +16,6 @@ require (
|
||||||
github.com/dgraph-io/ristretto v0.1.0
|
github.com/dgraph-io/ristretto v0.1.0
|
||||||
github.com/elnormous/contenttype v1.0.0
|
github.com/elnormous/contenttype v1.0.0
|
||||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||||
github.com/gchaincl/sqlhooks/v2 v2.0.1
|
|
||||||
github.com/go-chi/chi/v5 v5.0.3
|
github.com/go-chi/chi/v5 v5.0.3
|
||||||
github.com/go-fed/httpsig v1.1.0
|
github.com/go-fed/httpsig v1.1.0
|
||||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -111,8 +111,6 @@ github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW
|
||||||
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gchaincl/sqlhooks/v2 v2.0.1 h1:j9WZAq1Tx/xngDfEdsgUww+o9iY3rLOYGYhYWcFxdmI=
|
|
||||||
github.com/gchaincl/sqlhooks/v2 v2.0.1/go.mod h1:Qv7HXjGB9TehamVK52yW5H+a0RRhprIj3ESTcWOG2jw=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4=
|
github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4=
|
||||||
github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
@ -261,7 +259,6 @@ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECae
|
||||||
github.com/lestrrat-go/strftime v1.0.5 h1:A7H3tT8DhTz8u65w+JRpiBxM4dINQhUXAZnhBa2xeOE=
|
github.com/lestrrat-go/strftime v1.0.5 h1:A7H3tT8DhTz8u65w+JRpiBxM4dINQhUXAZnhBa2xeOE=
|
||||||
github.com/lestrrat-go/strftime v1.0.5/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
|
github.com/lestrrat-go/strftime v1.0.5/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
|
||||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
|
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
|
||||||
|
@ -272,7 +269,6 @@ github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
|
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
|
||||||
|
@ -292,7 +288,6 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
|
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
|
||||||
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
|
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
|
||||||
|
|
Loading…
Reference in New Issue