Goでの実用:データベースへのアクセス

数週間前、誰かがRedditに関する質問を作成しました:







GoがWebアプリケーションのコンテキストで(HTTPまたはその他の)ハンドラーでデータベースにアクセスするためのベストプラクティスとして何を使用しますか?

彼が受け取った回答は多様で興味深いものでした。 依存性注入の使用を提案する人もいれば、単純なグローバル変数の使用を支持する人もいれば、接続プールポインタをx / net / contextに置くことを提案する人もいました( コンテキストパッケージはgolang 1.7で使用されます)。







私はどうですか? 正解はプロジェクトに依存すると思います。







プロジェクトの全体的な構造とサイズはどのくらいですか? テストにはどのアプローチを使用しますか? このプロジェクトは将来どのような開発を受けますか? これらすべてのこと、その他多くのことが、どのアプローチがあなたに適しているかに部分的に影響します。







この投稿では、コードを整理し、データベース接続プールへのアクセスを構造化するための4つの異なるアプローチを検討します。







この投稿は、元の記事の無料翻訳です。 この記事の著者は、golangで書かれたアプリケーションでデータベースへのアクセスを整理するための4つのアプローチを提供しています







グローバル変数



最初に検討するアプローチは、一般的かつ単純です-データベースへの接続プールへのポインターを取得し、グローバル変数に入れます。







コードを美しく見せ、DRY原則(自分自身を繰り返さない-繰り返さない)に準拠させるために、初期化機能を使用して、他のパッケージとテストからグローバル接続プールを確立できます。







具体的な例が好きなので、オンラインストアデータベースと前回の投稿のコードを引き続き使用しましょう。 メインアプリケーションのHTTPハンドラーと、データベース、InitDB()関数、およびデータベースロジックのグローバル変数を含むモデルの個別のパッケージを使用して、MVC(Model View Controller)同様の構造を持つ単純なアプリケーションの作成を検討します。







bookstore ├── main.go └── models ├── books.go └── db.go
      
      





コード

ファイル:main.go







 package main import ( "bookstore/models" "fmt" "net/http" ) func main() { models.InitDB("postgres://user:pass@localhost/bookstore") http.HandleFunc("/books", booksIndex) http.ListenAndServe(":3000", nil) } func booksIndex(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, http.StatusText(405), 405) return } bks, err := models.AllBooks() if err != nil { http.Error(w, http.StatusText(500), 500) return } for _, bk := range bks { fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price) } }
      
      





ファイル:models / db.go







 package models import ( "database/sql" _ "github.com/lib/pq" "log" ) var db *sql.DB func InitDB(dataSourceName string) { var err error db, err = sql.Open("postgres", dataSourceName) if err != nil { log.Panic(err) } if err = db.Ping(); err != nil { log.Panic(err) } }
      
      





ファイル:models / books.go







 package models type Book struct { Isbn string Title string Author string Price float32 } func AllBooks() ([]*Book, error) { rows, err := db.Query("SELECT * FROM books") if err != nil { return nil, err } defer rows.Close() bks := make([]*Book, 0) for rows.Next() { bk := new(Book) err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price) if err != nil { return nil, err } bks = append(bks, bk) } if err = rows.Err(); err != nil { return nil, err } return bks, nil }
      
      





アプリケーションを実行し、/ booksでリクエストを実行すると、次のような応答が返されます。







 $ curl -i localhost:3000/books HTTP/1.1 200 OK Content-Length: 205 Content-Type: text/plain; charset=utf-8 978-1503261969, Emma, Jayne Austen, £9.44 978-1505255607, The Time Machine, HG Wells, £5.99 978-1503379640, The Prince, Niccolò Machiavelli, £6.99
      
      





グローバル変数の使用は、次の場合に潜在的に適しています。









上記の例では、グローバル変数を使用すると便利です。 しかし、データベースロジックが複数のパッケージで使用されている場合、より複雑なアプリケーションではどうなりますか?







1つのオプションはInitDBを数回呼び出すことですが、このアプローチはすぐに厄介になり、少し奇妙に見えます(実行時に空のポインターが呼び出されたときに接続プールを初期化し、パニックを取得するのは簡単です)。 2番目のオプションは、エクスポートされたデータベース変数で個別の構成パッケージを作成し、必要に応じて各ファイルに「yourproject / config」をインポートします。 危機にatしているものが明確でない場合は、例を見ることができます。







依存性注入



2番目のアプローチでは、依存性注入を検討します。 この例では、ポインターを接続プール、HTTPハンドラー、そしてデータベースロジックに明示的に渡します。







現実の世界では、アプリケーションにはおそらく、ハンドラーがアクセスできる要素を含む追加のレイヤー(競争上安全な)があります。 これらは、データベースへの接続のプールだけでなく、ロガーまたはキャッシュへのポインタにすることもできます。







すべてのハンドラーが同じパッケージにあるプロジェクトの場合、すてきなアプローチはすべての要素をカスタムEnvタイプにすることです。







 type Env struct { db *sql.DB logger *log.Logger templates *template.Template }
      
      





...そして、Envと同じ場所でハンドラーとメソッドを定義します。 これにより、ハンドラー用の接続プール(およびその他の要素)を作成するための明確で独特な方法が提供されます。







完全な例:







コード

ファイル:main.go







 package main import ( "bookstore/models" "database/sql" "fmt" "log" "net/http" ) type Env struct { db *sql.DB } func main() { db, err := models.NewDB("postgres://user:pass@localhost/bookstore") if err != nil { log.Panic(err) } env := &Env{db: db} http.HandleFunc("/books", env.booksIndex) http.ListenAndServe(":3000", nil) } func (env *Env) booksIndex(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, http.StatusText(405), 405) return } bks, err := models.AllBooks(env.db) if err != nil { http.Error(w, http.StatusText(500), 500) return } for _, bk := range bks { fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price) } }
      
      





ファイル:models / db.go







 package models import ( "database/sql" _ "github.com/lib/pq" ) func NewDB(dataSourceName string) (*sql.DB, error) { db, err := sql.Open("postgres", dataSourceName) if err != nil { return nil, err } if err = db.Ping(); err != nil { return nil, err } return db, nil }
      
      





ファイル:models / books.go







 package models import "database/sql" type Book struct { Isbn string Title string Author string Price float32 } func AllBooks(db *sql.DB) ([]*Book, error) { rows, err := db.Query("SELECT * FROM books") if err != nil { return nil, err } defer rows.Close() bks := make([]*Book, 0) for rows.Next() { bk := new(Book) err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price) if err != nil { return nil, err } bks = append(bks, bk) } if err = rows.Err(); err != nil { return nil, err } return bks, nil }
      
      





またはクロージャーを使用して...



Envでハンドラーとメソッドを定義したくない場合、別のアプローチは、クロージャーでハンドラーロジックを使用し、次のようにEnv変数を閉じることです。







コード

ファイル:main.go







 package main import ( "bookstore/models" "database/sql" "fmt" "log" "net/http" ) type Env struct { db *sql.DB } func main() { db, err := models.NewDB("postgres://user:pass@localhost/bookstore") if err != nil { log.Panic(err) } env := &Env{db: db} http.Handle("/books", booksIndex(env)) http.ListenAndServe(":3000", nil) } func booksIndex(env *Env) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, http.StatusText(405), 405) return } bks, err := models.AllBooks(env.db) if err != nil { http.Error(w, http.StatusText(500), 500) return } for _, bk := range bks { fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price) } }) }
      
      





依存性注入は、次の場合に適したアプローチです。









もう一度、ハンドラーとデータベースロジックが複数のパッケージに分散されている場合、このアプローチを使用できます。 これを達成する1つの方法は、エクスポートされたEnvタイプである個別の構成パッケージを作成することです。 上記の例でEnvを使用する1つの方法。 また、簡単な







インターフェースの使用



依存性注入の例を少し後で使用します。 モデルパッケージを変更して、カスタムデータベースタイプ( sql.DBを含む)を返しデータベースロジックをDBタイプとして実装しましょう。







二重の利点が得られます。まず、クリーンな構造を取得しますが、さらに重要なことは、単体テストの形式でデータベースをテストする可能性を開きます。







例を変更して、新しいDBタイプにいくつかのメソッドを実装する新しいDatastoreインターフェースを含めましょう。







 type Datastore interface { AllBooks() ([]*Book, error) }
      
      





アプリケーション全体でこのインターフェイスを使用できます。 例を更新しました。







コード

ファイル:main.go







 package main import ( "fmt" "log" "net/http" "bookstore/models" ) type Env struct { db models.Datastore } func main() { db, err := models.NewDB("postgres://user:pass@localhost/bookstore") if err != nil { log.Panic(err) } env := &Env{db} http.HandleFunc("/books", env.booksIndex) http.ListenAndServe(":3000", nil) } func (env *Env) booksIndex(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, http.StatusText(405), 405) return } bks, err := env.db.AllBooks() if err != nil { http.Error(w, http.StatusText(500), 500) return } for _, bk := range bks { fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price) } }
      
      





ファイル:models / db.go







 package models import ( _ "github.com/lib/pq" "database/sql" ) type Datastore interface { AllBooks() ([]*Book, error) } type DB struct { *sql.DB } func NewDB(dataSourceName string) (*DB, error) { db, err := sql.Open("postgres", dataSourceName) if err != nil { return nil, err } if err = db.Ping(); err != nil { return nil, err } return &DB{db}, nil }
      
      





ファイル:models / books.go







 package models type Book struct { Isbn string Title string Author string Price float32 } func (db *DB) AllBooks() ([]*Book, error) { rows, err := db.Query("SELECT * FROM books") if err != nil { return nil, err } defer rows.Close() bks := make([]*Book, 0) for rows.Next() { bk := new(Book) err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price) if err != nil { return nil, err } bks = append(bks, bk) } if err = rows.Err(); err != nil { return nil, err } return bks, nil }
      
      





ハンドラーがDatastoreインターフェースを使用するようになったため、データベースからの応答の単体テストを簡単に作成できます。







コード
 package main import ( "bookstore/models" "net/http" "net/http/httptest" "testing" ) type mockDB struct{} func (mdb *mockDB) AllBooks() ([]*models.Book, error) { bks := make([]*models.Book, 0) bks = append(bks, &models.Book{"978-1503261969", "Emma", "Jayne Austen", 9.44}) bks = append(bks, &models.Book{"978-1505255607", "The Time Machine", "HG Wells", 5.99}) return bks, nil } func TestBooksIndex(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/books", nil) env := Env{db: &mockDB{}} http.HandlerFunc(env.booksIndex).ServeHTTP(rec, req) expected := "978-1503261969, Emma, Jayne Austen, £9.44\n978-1505255607, The Time Machine, HG Wells, £5.99\n" if expected != rec.Body.String() { t.Errorf("\n...expected = %v\n...obtained = %v", expected, rec.Body.String()) } }
      
      





リクエストスコープのコンテキスト



最後に、リクエストのスコープでコンテキストを使用し、データベース接続プールを転送する方法を見てみましょう。 特に、x / net / contextパッケージを使用します。







個人的に、私はリクエストの範囲のコンテキストでアプリケーションレベルの変数のファンではありません-それは私にとって厄介で負担が大きいように見えます。 x / net / contextパッケージのドキュメントでは、次のことも推奨しています。







コンテキスト値は、プロセスおよびAPIエントリポイントが渡すリクエスト内のデータのスコープに対してのみ使用し、オプションのパラメーターを関数に渡すためには使用しません。

ただし、人々はこのアプローチを使用します。 プロジェクトに多くのパッケージが含まれており、グローバル構成の使用について説明していない場合、これは非常に魅力的なソリューションです。







Joe Shawのすばらしい記事で提案されているテンプレートを使用してハンドラーにコンテキストを渡し、書店の例を最後に適合させましょう。







コード

ファイル:main.go







 package main import ( "bookstore/models" "fmt" "golang.org/x/net/context" "log" "net/http" ) type ContextHandler interface { ServeHTTPContext(context.Context, http.ResponseWriter, *http.Request) } type ContextHandlerFunc func(context.Context, http.ResponseWriter, *http.Request) func (h ContextHandlerFunc) ServeHTTPContext(ctx context.Context, rw http.ResponseWriter, req *http.Request) { h(ctx, rw, req) } type ContextAdapter struct { ctx context.Context handler ContextHandler } func (ca *ContextAdapter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { ca.handler.ServeHTTPContext(ca.ctx, rw, req) } func main() { db, err := models.NewDB("postgres://user:pass@localhost/bookstore") if err != nil { log.Panic(err) } ctx := context.WithValue(context.Background(), "db", db) http.Handle("/books", &ContextAdapter{ctx, ContextHandlerFunc(booksIndex)}) http.ListenAndServe(":3000", nil) } func booksIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, http.StatusText(405), 405) return } bks, err := models.AllBooks(ctx) if err != nil { http.Error(w, http.StatusText(500), 500) return } for _, bk := range bks { fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price) } }
      
      





ファイル:models / db.go







 package models import ( "database/sql" _ "github.com/lib/pq" ) func NewDB(dataSourceName string) (*sql.DB, error) { db, err := sql.Open("postgres", dataSourceName) if err != nil { return nil, err } if err = db.Ping(); err != nil { return nil, err } return db, nil }
      
      





ファイル:models / books.go







 package models import ( "database/sql" "errors" "golang.org/x/net/context" ) type Book struct { Isbn string Title string Author string Price float32 } func AllBooks(ctx context.Context) ([]*Book, error) { db, ok := ctx.Value("db").(*sql.DB) if !ok { return nil, errors.New("models: could not get database connection pool from context") } rows, err := db.Query("SELECT * FROM books") if err != nil { return nil, err } defer rows.Close() bks := make([]*Book, 0) for rows.Next() { bk := new(Book) err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price) if err != nil { return nil, err } bks = append(bks, bk) } if err = rows.Err(); err != nil { return nil, err } return bks, nil }
      
      





PS翻訳の作者は、翻訳の指摘された誤りと不正確さに感謝します。








All Articles