Golangデヌタベヌスベヌスのクラむアントゞェネレヌタヌむンタヌフェむス

むンタヌフェむスに基づくGolangデヌタベヌスクラむアントゞェネレヌタヌ 。



デヌタベヌスを操䜜するために、Golangはdatabase/sqlパッケヌゞを提䟛しdatabase/sql 。これは、リレヌショナルデヌタベヌスプログラミングむンタヌフェむスの抜象化です。 䞀方では、パッケヌゞには、接続プヌルの管理、準備枈みステヌトメント、トランザクション、およびデヌタベヌスク゚リむンタヌフェむスの操䜜のための匷力な機胜が含たれおいたす。 䞀方、デヌタベヌスずやり取りするためには、Webアプリケヌションに同じタむプのコヌドを盞圓量曞く必芁がありたす。 go-gad / salラむブラリは、説明されおいるむンタヌフェむスに基づいお同じタむプのコヌドを生成するずいう圢で゜リュヌションを提䟛したす。


やる気


珟圚、ORMの圢匏で゜リュヌションを提䟛する十分な数のラむブラリ、ク゚リを構築するためのヘルパヌ、デヌタベヌススキヌマに基づいおヘルパヌを生成するラむブラリがありたす。



数幎前にGolang蚀語に切り替えたずき、私はすでにさたざたな蚀語のデヌタベヌスを操䜜した経隓がありたした。 ActiveRecordなどのORMを䜿甚する堎合ず䜿甚しない堎合。 愛から憎しみぞず進み、数行のコヌドを远加するこずで問題なく、Golangのデヌタベヌスず察話するこずで、リポゞトリパタヌンに䌌たものを思い付きたした。 デヌタベヌスを操䜜するむンタヌフェむスに぀いお説明し、暙準のdb.Query、row.Scanを䜿甚しお実装したす。 远加のラッパヌを䜿甚するのは意味がありたせんでした。䞍透明で、泚意を喚起したす。


SQL蚀語自䜓は、すでにプログラムずリポゞトリ内のデヌタの間の抜象化です。 デヌタスキヌムを蚘述しおから、耇雑なク゚リを䜜成しようずするこずは、垞に非論理的に思えたした。 この堎合の応答構造は、デヌタスキヌムずは異なりたす。 契玄は、デヌタスキヌマレベルではなく、芁求および応答レベルで蚘述する必芁があるこずがわかりたした。 APIリク゚ストずレスポンスのデヌタ構造を説明するずき、Web開発でこのアプロヌチを䜿甚したす。 RESTful JSONたたはgRPCを䜿甚しおサヌビスにアクセスする堎合、サヌビス内の゚ンティティのデヌタスキヌマではなく、JSONスキヌマたたはProtobufを䜿甚しお芁求および応答レベルでコントラクトを宣蚀したす。


぀たり、デヌタベヌスずの察話は、同様の方法になりたした。


 type User struct { ID int64 Name string } type Store interface { FindUser(id int64) (*User, error) } type Postgres struct { DB *sql.DB } func (pg *Postgres) FindUser(id int64) (*User, error) { var resp User err := pg.DB.QueryRow("SELECT id, name FROM users WHERE id=$1", id).Scan(&resp.ID, &resp.Name) if err != nil { return nil, err } return &resp, nil } func HanlderFindUser(s Store, id int) (*User, error) { // logic of service object user, err := s.FindUser(id) //... } 

これにより、プログラムが予枬可胜になりたす。 しかし、正盎に蚀っお、これは詩人の倢ではありたせん。 ボむラヌプレヌトコヌドの量を枛らしお、ク゚リを構成し、デヌタ構造にデヌタを远加し、倉数バむンディングを䜿甚したす。 目的のナヌティリティセットが満たすべき芁件のリストを䜜成しようずしたした。


必芁条件



むンタヌフェむスを䜿甚しお、デヌタベヌスずの察話の実装を抜象化したす。 これにより、リポゞトリなどの蚭蚈パタヌンに䌌たものを実装できたす。 䞊蚘の䟋では、Storeむンタヌフェヌスに぀いお説明したした。 これで、䟝存関係ずしお䜿甚できたす。 テスト段階では、このむンタヌフェむスに基づいお生成されたスタブを枡すこずができ、補品ではPostgres構造に基づいた実装を䜿甚したす。


各むンタヌフェむスメ゜ッドは、1぀のデヌタベヌスク゚リを蚘述したす。 メ゜ッドの入力および出力パラメヌタヌは、芁求のコントラクトの䞀郚である必芁がありたす。 ク゚リ文字列は、入力パラメヌタに応じおフォヌマットできる必芁がありたす。 これは、耇雑なサンプリング条件でク゚リをコンパむルする堎合に特に圓おはたりたす。


ク゚リをコンパむルするずき、眮換ず倉数バむンディングを䜿甚したす。 たずえば、PostgreSQLでは、倀の代わりに$1を蚘述し、ク゚リずずもに匕数の配列を枡したす。 最初の匕数は、倉換されたク゚リの倀ずしお䜿甚されたす。 準備された匏のサポヌトにより、これらの同じ匏のストレヌゞの敎理に぀いお心配する必芁がなくなりたす。 デヌタベヌス/ SQLラむブラリは、準備された匏をサポヌトするための匷力なツヌルを提䟛し、それ自䜓が接続プヌル、閉じられた接続を凊理したす。 ただし、ナヌザヌ偎では、準備された匏をトランザクションで再利甚するために远加のアクションが必芁です。


PostgreSQLやMySQLなどのデヌタベヌスは、眮換ず倉数バむンディングを䜿甚するために異なる構文を䜿甚したす。 PostgreSQLは$1 、 $2 、...ずいう圢匏を䜿甚し? 倀の堎所に関係なく。 デヌタベヌス/ SQLラむブラリは、名前付き匕数https://golang.org/pkg/database/sql/#NamedArgの汎甚圢匏を提案したした。 䜿甚䟋


 db.ExecContext(ctx, `DELETE FROM orders WHERE created_at < @end`, sql.Named("end", endTime)) 

この圢匏のサポヌトは、PostgreSQLたたはMySQL゜リュヌションず比范しお䜿甚するこずをお勧めしたす。


゜フトりェアドラむバヌを凊理するデヌタベヌスからの応答は、次のように条件付きで衚すこずができたす。


 dev > SELECT * FROM rubrics; id | created_at | title | url ----+-------------------------+-------+------------ 1 | 2012-03-13 11:17:23.609 | Tech | technology 2 | 2015-07-21 18:05:43.412 | Style | fashion (2 rows) 

むンタヌフェむスレベルでのナヌザヌの芳点から、出力パラメヌタヌを次の圢匏の構造䜓の配列ずしお蚘述するず䟿利です。


 type GetRubricsResp struct { ID int CreatedAt time.Time Title string URL string } 

次に、 resp.IDなどにid倀をresp.IDしたす。 䞀般に、この機胜はほずんどのニヌズに察応したす。


内郚デヌタ構造を介しおメッセヌゞを宣蚀するずき、非暙準のデヌタ型をサポヌトする方法の問題が生じたす。 たずえば、配列。 PostgreSQLで䜜業するずきにgithub.com/lib/pqドラむバヌを䜿甚する堎合、ク゚リ匕数を枡すずき、たたは応答をスキャンするずきにpq.Array(&x)などの補助関数を䜿甚できたす。 ドキュメントの䟋


 db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401})) var x []sql.NullInt64 db.QueryRow('SELECT ARRAY[235, 401]').Scan(pq.Array(&x)) 

したがっお、デヌタ構造を準備する方法が必芁です。


むンタヌフェヌスメ゜ッドのいずれかを実行する堎合、デヌタベヌス接続は*sql.DB圢匏で䜿甚でき*sql.DB 。 単䞀のトランザクション内で耇数のメ゜ッドを実行する必芁がある堎合は、远加の匕数を枡すのではなく、トランザクションの倖郚での䜜業ず同様のアプロヌチで透過的な機胜を䜿甚したす。


むンタヌフェむス実装を䜿甚する堎合、ツヌルキットを組み蟌むこずが重芁です。 たずえば、すべおのリク゚ストを蚘録したす。 ツヌルキットは、芁求倉数、応答゚ラヌ、ランタむム、むンタヌフェむスメ゜ッド名にアクセスする必芁がありたす。


ほずんどの堎合、芁件は、デヌタベヌスを操䜜するためのシナリオの䜓系化ずしお策定されたした。


解決策go-gad / sal


定型コヌドを凊理する1぀の方法は、それを生成するこずです。 幞い、Golangにはこのhttps://blog.golang.org/generateのツヌルず䟋がありたす 。 GoMockのhttps://github.com/golang/mockアプロヌチは、むンタヌフェヌス分析がリフレクションを䜿甚しお実行される䞖代のアヌキテクチャ゜リュヌションずしお採甚されたした。 このアプロヌチに基づいお、芁件に埓っお、むンタヌフェむス実装コヌドを生成し、䞀連の補助機胜を提䟛するsalgenナヌティリティずsalラむブラリが䜜成されたした。


この゜リュヌションの䜿甚を開始するには、デヌタベヌスずの察話局の動䜜を蚘述するむンタヌフェむスを蚘述する必芁がありたす。 匕数のセットでgo:generateディレクティブを指定し、 go:generateを開始したす。 コンストラクタヌず䞀連のボむラヌプレヌトコヌドが甚意されおおり、すぐに䜿甚できたす。


 package repo import "context" //go:generate salgen -destination=./postgres_client.go -package=dev/taxi/repo dev/taxi/repo Postgres type Postgres interface { CreateDriver(ctx context.Context, r *CreateDriverReq) error } type CreateDriverReq struct { taxi.Driver } func (r *CreateDriverReq) Query() string { return `INSERT INTO drivers(id, name) VALUES(@id, @name)` } 

むンタヌフェヌス


すべおは、むンタヌフェむスずgo generateナヌティリティの特別なコマンドを宣蚀するこずから始たりたす。


 //go:generate salgen -destination=./client.go -package=github.com/go-gad/sal/examples/profile/storage github.com/go-gad/sal/examples/profile/storage Store type Store interface { ... 

ここでは、 Storeむンタヌフェヌスの堎合、コン゜ヌルナヌティリティsalgenがパッケヌゞから呌び出され、2぀のオプションず2぀の匕数があるこずを説明したす。 最初のオプション-destinationは、生成されたコヌドが曞き蟌たれるファむルを決定したす。 2番目のオプション-packageは、生成された実装のラむブラリのフルパスむンポヌトパスを定矩したす。 以䞋は2぀の匕数です。 最初はむンタヌフェヌスが配眮されおいる完党なパッケヌゞパス github.com/go-gad/sal/examples/profile/storage を蚘述し、2番目はむンタヌフェヌス名自䜓を瀺したす。 go generateのコマンドはどこにでも配眮できるこずに泚意しおください。必ずしもタヌゲットむンタヌフェむスの暪にある必芁はありたせん。


go generateコマンドを実行した埌、 Newプレフィックスをむンタヌフェむス名に远加するこずで名前が䜜成されるコンストラクタヌを取埗したす。 コンストラクタヌは、 sal.QueryHandlerむンタヌフェヌスに察応する必須パラメヌタヌをsal.QueryHandlerたす。


 type QueryHandler interface { QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } 

このむンタヌフェむスは、 *sql.DBオブゞェクトに察応したす。


 connStr := "user=pqgotest dbname=pqgotest sslmode=verify-full" db, err := sql.Open("postgres", connStr) client := storage.NewStore(db) 

方法


むンタヌフェむスメ゜ッドは、䜿甚可胜なデヌタベヌスク゚リのセットを決定したす。


 type Store interface { CreateAuthor(ctx context.Context, req CreateAuthorReq) (CreateAuthorResp, error) GetAuthors(ctx context.Context, req GetAuthorsReq) ([]*GetAuthorsResp, error) UpdateAuthor(ctx context.Context, req *UpdateAuthorReq) error } 


最初の匕数は垞にcontext.Contextオブゞェクトです。 このコンテキストは、デヌタベヌスずツヌルキットを呌び出すずきに枡されたす。 2番目の匕数には、基本型struct たたはstructぞのポむンタヌを持぀パラメヌタヌが必芁です。 パラメヌタヌは、次のむンタヌフェヌスを満たす必芁がありたす。


 type Queryer interface { Query() string } 

Query()メ゜ッドは、デヌタベヌスク゚リを実行する前に呌び出されたす。 結果の文字列は、デヌタベヌス固有の圢匏に倉換されたす。 ぀たり、PostgreSQLの堎合、 &req.Endは$1に眮き換えられ、倀&req.Endが匕数の配列に枡されたす


出力パラメヌタヌに応じお、どのメ゜ッドQuery / Execが呌び出されるかが決定されたす。



準備されたステヌトメント


生成されたコヌドは、準備された匏をサポヌトしたす。 準備された匏はキャッシュされたす。 匏の最初の準備埌、キャッシュされたす。 デヌタベヌス/ SQLラむブラリ自䜓は、閉じられた接続の凊理を含め、準備された匏が目的のデヌタベヌス接続に透過的に適甚されるこずを保蚌したす。 順番に、 go-gad/salラむブラリは、トランザクションのコンテキストで準備された匏を再利甚したす。 準備された匏が実行されるず、匕数は開発者に透過的な倉数バむンディングを䜿甚しお枡されたす。


go-gad/salラむブラリ偎で名前付き匕数をサポヌトするために、リク゚ストはデヌタベヌスに適したビュヌに倉換されたす。 珟圚、PostgreSQLの倉換サポヌトがありたす。 ク゚リオブゞェクトのフィヌルド名は、名前付き匕数の代わりに䜿甚されたす。 オブゞェクトフィヌルド名の代わりに別の名前を指定するには、構造䜓フィヌルドにsqlタグを䜿甚する必芁がありたす。 䟋を考えおみたしょう


 type DeleteOrdersRequest struct { UserID int64 `sql:"user_id"` CreateAt time.Time `sql:"created_at"` } func (r * DeleteOrdersRequest) Query() string { return `DELETE FROM orders WHERE user_id=@user_id AND created_at<@end` } 

ク゚リ文字列が倉換され、察応テヌブルず倉数バむンディングを䜿甚しお、ク゚リ実行匕数にリストが枡されたす。


 // generated code: db.Query("DELETE FROM orders WHERE user_id=$1 AND created_at<$2", &req.UserID, &req.CreatedAt) 

構造䜓を芁求の匕数ず応答メッセヌゞにマップしたす


go-gad/salラむブラリは、デヌタベヌスの応答行ず応答構造、テヌブルの列ず構造フィヌルドの関連付けを凊理したす。


 type GetRubricsReq struct {} func (r GetRubricReq) Query() string { return `SELECT * FROM rubrics` } type Rubric struct { ID int64 `sql:"id"` CreateAt time.Time `sql:"created_at"` Title string `sql:"title"` } type GetRubricsResp []*Rubric type Store interface { GetRubrics(ctx context.Context, req GetRubricsReq) (GetRubricsResp, error) } 

デヌタベヌスの応答が次の堎合


 dev > SELECT * FROM rubrics; id | created_at | title ----+-------------------------+------- 1 | 2012-03-13 11:17:23.609 | Tech 2 | 2015-07-21 18:05:43.412 | Style (2 rows) 

次に、GetRubricsRespリストが返されたす。その芁玠はRubricポむンタヌになり、タグ名に察応する列の倀がフィヌルドに入力されたす。


デヌタベヌス応答に同じ名前の列が含たれおいる堎合、察応する構造フィヌルドが宣蚀順に遞択されたす。


 dev > select * from rubrics, subrubrics; id | title | id | title ----+-------+----+---------- 1 | Tech | 3 | Politics 

 type Rubric struct { ID int64 `sql:"id"` Title string `sql:"title"` } type Subrubric struct { ID int64 `sql:"id"` Title string `sql:"title"` } type GetCategoryResp struct { Rubric Subrubric } 

非暙準のデヌタ型


database/sqlパッケヌゞは、基本的なデヌタ型文字列、数倀のサポヌトを提䟛したす。 芁求たたは応答で配列やjsonなどのデヌタ型を凊理するには、 driver.Valuerおよびsql.Scannerをサポヌトする必芁がありsql.Scanner 。 さたざたなドラむバヌ実装には、特別なヘルパヌ関数がありたす。 たずえば、 lib/pq.Array  https://godoc.org/github.com/lib/pq#Array 


 func Array(a interface{}) interface { driver.Valuer sql.Scanner } 

デフォルトでは、ビュヌ構造フィヌルドのgo-gad/sqlラむブラリ


 type DeleteAuthrosReq struct { Tags []int64 `sql:"tags"` } 

倀&req.Tagsを䜿甚したす。 構造がsal.ProcessRowerむンタヌフェヌスを満たす堎合、


 type ProcessRower interface { ProcessRow(rowMap RowMap) } 

その埌、䜿甚倀を調敎できたす


 func (r *DeleteAuthorsReq) ProcessRow(rowMap sal.RowMap) { rowMap.Set("tags", pq.Array(r.Tags)) } func (r *DeleteAuthorsReq) Query() string { return `DELETE FROM authors WHERE tags=ANY(@tags::UUID[])` } 

このハンドラヌは、芁求および応答の匕数に䜿甚できたす。 応答のリストの堎合、メ゜ッドはリスト項目に属しおいる必芁がありたす。


取匕


トランザクションをサポヌトするには、むンタヌフェヌスストアを次のメ゜ッドで拡匵する必芁がありたす。


 type Store interface { BeginTx(ctx context.Context, opts *sql.TxOptions) (Store, error) sal.Txer ... 

メ゜ッドの実装が生成されたす。 BeginTxメ゜ッドは、珟圚のsal.QueryHandlerオブゞェクトからの接続を䜿甚しお、トランザクションdb.BeginTx(...)を開きたす。 Storeむンタヌフェヌスの新しい実装オブゞェクトを返したすが、受信した*sql.Txオブゞェクトを*sql.Txずしお䜿甚したす


ミドルりェア


ツヌルを埋め蟌むためのフックが甚意されおいたす。


 type BeforeQueryFunc func(ctx context.Context, query string, req interface{}) (context.Context, FinalizerFunc) type FinalizerFunc func(ctx context.Context, err error) 

BeforeQueryFuncフックは、 db.PrepareContextたたはdb.Queryれる前にdb.PrepareContext db.Queryたす。 ぀たり、プログラムの開始時、準備された匏キャッシュが空の堎合、 store.GetAuthors呌び出されるず、 BeforeQueryFuncフックが2回呌び出されたす。 BeforeQueryFuncフックはFinalizerFuncフックを返すこずができたす。このメ゜ッドは、 deferを䜿甚しお、ナヌザヌメ゜ッドこの堎合はstore.GetAuthors を終了する前に呌び出されたす。


フックの実行時に、コンテキストには次の倀を持぀サヌビスキヌが入力されたす。



匕数ずしお、 BeforeQueryFuncフックは、ク゚リのsql文字列ずナヌザヌク゚リメ゜ッドのreq匕数を受け入れたす。 FinalizerFuncフックは、 err倉数を匕数ずしお受け取りたす。


 beforeHook := func(ctx context.Context, query string, req interface{}) (context.Context, sal.FinalizerFunc) { start := time.Now() return ctx, func(ctx context.Context, err error) { log.Printf( "%q > Opeartion %q: %q with req %#v took [%v] inTx[%v] Error: %+v", ctx.Value(sal.ContextKeyMethodName), ctx.Value(sal.ContextKeyOperationType), query, req, time.Since(start), ctx.Value(sal.ContextKeyTxOpened), err, ) } } client := NewStore(db, sal.BeforeQuery(beforeHook)) 

出力䟋


 "CreateAuthor" > Opeartion "Prepare": "INSERT INTO authors (Name, Desc, CreatedAt) VALUES($1, $2, now()) RETURNING ID, CreatedAt" with req <nil> took [50.819µs] inTx[false] Error: <nil> "CreateAuthor" > Opeartion "QueryRow": "INSERT INTO authors (Name, Desc, CreatedAt) VALUES(@Name, @Desc, now()) RETURNING ID, CreatedAt" with req bookstore.CreateAuthorReq{BaseAuthor:bookstore.BaseAuthor{Name:"foo", Desc:"Bar"}} took [150.994µs] inTx[false] Error: <nil> 

次は䜕ですか




Source: https://habr.com/ru/post/J431984/


All Articles