前回(というか、今朝)、goquというGoのSQLのクエリビルダーを紹介しましたが、カラム名をハードコーディングするので、カラムに変更があると、どこに書かれてるのか探すのが大変ですね。grep、目grepで対応して、見えないバグが仕込まれるかもしれません。
そして、人(僕)はよくtypoする。
なので、information_schema
から、カラム名やテーブル名を取得してコード生成してしまえば良いのではと考えました。
Before, After
実際どんなイメージになるかというと。
Before:
ex = goqu.Or( goqu.And( goqu.C("first_name").Eq(firstName), goqu.C("age").Lt(20), ), goqu.And{ goqu.C("first_name").Eq(firstName), goqu.C("age").Gt(30), }, ) query, args := goqu.Dialect("mysql").From("user").Where(ex)
カラム名のage
や first_name
、テーブル名のuser
をハードコードしています。
After:
import ( td "your-project/table-definition" // 適当です。 ) d := td.SchemaName.Table ex = goqu.Or( goqu.And( goqu.C(d.FirstName()).Eq(firstName), goqu.C(d.Age()).Lt(20), ), goqu.And{ goqu.C(d.FirstName()).Eq(firstName), goqu.C(d.Age()).Gt(30), }, ) query, args := goqu.Dialect("mysql").From(d.Tablename()).Where(ex)
という感じに、メソッド呼び出しで書けるようになります。
(tablename
というカラム名があったら、メソッド名とかぶりますが、そんなカラム名は付けないでしょう。つけたとしても、常識的にtable_name
と命名すれば、TableName
になるから問題ない)
自動生成でやっていること
"your-project/table-definition" に、var SchemaName
を定義します。そこに渡しているtypeは、以下のようなものです。
type tableName1 struct {} func (tableName1) Tablename () string { return "table_name" } func (tableName1) ColumnName1 () string { return "column_name1" } func (tableName1) ColumnName2 () string { return "column_name2" } type schemaName struct { TableName1 tableNaem1 TableName2 tableNaem2 } var SchemaName = schemaName{ tableName1{}, tableName2{}, }
手で書くなら、気が狂いそうになりますが、まぁ、自動生成なら許せる。
生成するコード
生成するコード自体は特に難しいことはやっておらず、information_schema
のCOLUMNS
テーブルを検索して、typeとfuncを作りまくるという単純なコードになります。
package main import ( "fmt" "log" "os" "pmall-api/internal/config" "pmall-api/internal/infrastructure/persistence/db" "strings" "github.com/iancoleman/strcase" ) func main() { if len(os.Args) != 4 { log.Fatal("pass dsn, package name and database names with comma") } dsn := os.Args[1] packageName := os.Args[2] databases := make([]string, 0) for _, name := range strings.Split(os.Args[3], ",") { databases = append(databases, "'"+name+"'") } database, err := db.NewConnection(dsn) if err != nil { log.Fatal(err) } result, err := database.Query( fmt.Sprintf(` SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA in (%s) ORDER BY TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME `, strings.Join(databases, ",")), ) if err != nil { log.Fatal(err) } fmt.Printf("package %s\n\n", packageName) currentSchema := "" currentTable := "" tables := make([]string, 0) if !result.Next() { log.Fatal("no rows") } for { var tableSchema string var tableName string var columnName string err := result.Scan(&tableSchema, &tableName, &columnName) if err != nil { log.Fatal(err) } if currentSchema == "" { currentSchema = tableSchema } fullName := tableSchema + strcase.ToCamel(tableName) if currentTable != tableName { tables = append(tables, tableName) fmt.Printf("type %s struct {}\n\n", fullName) fmt.Printf("func (%s) Tablename() string { return \"%s\" } \n\n", fullName, tableName) currentTable = tableName } fmt.Printf("func (%s) %s() string { return \"%s\"}\n\n", fullName, strcase.ToCamel(columnName), columnName, ) hasNext := result.Next() if !hasNext || currentSchema != tableSchema { fmt.Printf("type %sDB struct {\n", tableSchema) structs := make([]string, 0) for _, table := range tables { fullName := tableSchema + strcase.ToCamel(table) fmt.Printf(" %s %s\n", strcase.ToCamel(table), fullName) structs = append(structs, fullName+"{}") } fmt.Print("}\n\n") fmt.Printf("var %s = &%sDB{\n %s,\n}", strcase.ToCamel(currentSchema), currentSchema, strings.Join(structs, ",\n ")) tables = make([]string, 0) } if !hasNext { break } } }
終わり
これで、typoや、カラム変更も怖くないですね。
めでたし。