Practice of Programming

プログラム とか Linuxとかの話題

Goでテーブル情報を自動生成してgoquで使う

前回(というか、今朝)、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)

カラム名agefirst_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_schemaCOLUMNSテーブルを検索して、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や、カラム変更も怖くないですね。

めでたし。