Golangとデータベースインタラクションの進化

この記事では、golangを実行しているデータベースを操作する際の問題を要約します。 単純な問題を解決するとき、これらの問題は通常目に見えません。 プロジェクトが大きくなると、問題も大きくなります。 それらの最も話題の:









この記事は、github.com / adverax / echo / database / sqlパッケージに基づいています。 このパッケージを使用するセマンティクスは、標準のデータベース/ sqlパッケージに可能な限り近いため、使用に問題があるとは思いません。







範囲



原則として、大規模なシステムは、システムの各コンポーネントの責任の明確な領域と緩やかに接続しようとします。 したがって、パブリッシャー/サブスクライバーの設計パターンは広く実践されています。 システムに新しいユーザーを登録する小さな例を考えてみましょう。







package main import "database/sql" type User struct { Id int64 Name string Language string } type Manager struct { DB *sql.DB OnSignup func(db *sql.DB, user *User) error } func (m *Manager) Signup(user *User) (id int64, err error) { id, err = m.insert(user) if err != nil { return } user.Id = id err = m.OnSignup(m.DB, user) return } func (m *Manager) insert(user *User) (int64, error) { res, err := m.DB.Exec("INSERT ...") if err != nil { return 0, err } id, err := res.LastInsertId() if err != nil { return 0, err } return id, err } func main() { manager := &Manager{ // ... OnSignup: func(db *sql.DB, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } }
      
      





この例では、主にOnSignupイベントに関心があります。 単純化するために、ハンドラーは単一の関数で表されます(実際には、すべてがより複雑です)。 イベントシグネチャでは、最初のパラメーターのタイプを厳密に規定しますが、通常は広範囲に及ぶ結果になります。

ここでアプリケーションの機能を拡張し、ユーザー登録が成功した場合に彼の個人アカウントにメッセージを送信するとします。 理想的には、メッセージはユーザー登録と同じトランザクションに配置する必要があります。







 type Manager struct { DB *sql.DB OnSignup func(tx *sql.Tx, user *User) error } func (m *Manager) Signup(user *User) error { tx, err := m.DB.Begin() if err != nil { return err } defer tx.Rollback() id, err := m.insert(user) if err != nil { return err } user.Id = id err = m.OnSignup(tx, id) if err != nil { return err } return tx.Commit() } func main() { manager := &Manager{ // ... OnSignup: func(db *sql.Tx, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } }
      
      





例からわかるように、イベントの署名を変更する必要がありました。 このソリューションはクリーンではなく、ハンドラーがデータベースクエリの実行のコンテキストを知っていることを意味します。 よりクリーンなソリューションは、汎用データベースとトランザクションインターフェイスを使用することです-スコープ。







 import "github.com/adverax/echo/database/sql" type Manager struct { DB sql.DB OnSignup func(scope sql.Scope, user *User) error } func (m *Manager) Signup(user *User) error { tx, err := m.DB.Begin() if err != nil { return err } defer tx.Rollback() id, err := m.insert(user) if err != nil { return err } err = m.OnSignup(tx, id) if err != nil { return err } return tx.Commit() } func main() { manager := &Manager{ // ... OnSignup: func(scope sql.Scope, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } }
      
      





このアプローチを実装するには、ハンドラーがトランザクションを使用できるため、ネストされたトランザクションのサポートが必要です。 幸い、ほとんどのDBMSはSAVEPOINTメカニズムをサポートしているため、これは問題ではありません。







データベースとコンテキスト



通常、データベースへの接続は上記のようにパラメーターとして渡されず、各マネージャーはデータベースへの接続へのリンクを保持します。 これにより、メソッドシグネチャが簡素化され、コードが読みやすくなります。 この場合、トランザクションへのリンクを転送する必要があるため、これを回避することはできません。

コンテキストはパススルーパラメーターとして配置されるため、かなりエレガントなソリューションは、トランザクション(スコープ)へのリンクをコンテキストに配置することです。 その後、コードをさらに簡素化できます。







 import ( "context" "github.com/adverax/echo/database/sql" ) type Manager struct { sql.Repository OnSignup func(ctx context.Context, user *User) error } func (m *Manager) Signup(ctx context.Context, user *User) error { return m.Transaction( ctx, func(ctx context.Context, scope sql.Scope) error { id, err := m.insert(user) if err != nil { return err } user.Id = id return m.OnSignup(ctx, user) }, ) } type Messenger struct { sql.Repository } func(messenger *Messenger) onSignupUser(ctx context.Context, user *User) error { _, err := messenger.Scope(ctx).Exec("INSERT ...") return err } func main() { db := ... messenger := &Messenger{ Repository: sql.NewRepository(db), } manager := &Manager{ Repository: sql.NewRepository(db), OnSignup: messenger.onSignup, } err := manager.Signup(&User{...}) if err != nil { panic(err) } }
      
      





この例は、マネージャーの完全な分離を維持し、コードの可読性を高め、単一のスコープでそれらの共同作業を達成したことを示しています。







レプリケーションのサポート



このライブラリは、レプリケーションの使用もサポートしています。 タイプExecのすべてのリクエストはマスターに送信されます。 スレーブタイプのリクエストは、ランダムに選択されたスレーブに転送されます。 レプリケーションをサポートするには、いくつかのデータソースを指定するだけです。







 func work() { dsc := &sql.DSC{ Driver: "mysql", DSN: []*sql.DSN{ { Host: "127.0.0.1", Database: "echo", Username: "root", Password: "password", }, { Host: "192.168.44.01", Database: "echo", Username: "root", Password: "password", }, }, } db := dsc.Open(nil) defer db.Close() ... }
      
      





データベースを開くときに単一のデータソースを使用する場合、追加のオーバーヘッドなしで通常モードで開きます。







指標



ご存知のように、メトリックは安価であり、ログは高価です。 したがって、デフォルトのメトリックのサポートを追加することが決定されました。







クエリのプロファイリングとログ



デバッグ中にデータベースクエリをログに記録することが非常に必要です。 ただし、本番環境でオーバーヘッドのない高品質のロギングメカニズムは見たことがありません。 このライブラリを使用すると、データベースをラップすることでこの問題をエレガントに解決できます。 データベースのプロファイルを作成するには、適切なアクティベーターをデータベースに渡すだけです。







 func openDatabase(dsc sql.DSC, debug bool) (sql.DB, error){ if debug { return dsc.Open(sql.OpenWithProfiler(nil, "", nil)) } return dsc.Open(nil) } func main() { dsc := ... db, err := openDatabase(dsc, true) if err != nil { panic(err) } defer db.Close() ... }
      
      





おわりに



提案されたパッケージを使用すると、不要な詳細を隠しながら、データベースと対話する可能性を拡張できます。 これにより、アプリケーションの複雑さが増しているにもかかわらず、コードの質を向上させ、疎結合と透過性を保つことができます。








All Articles