goを学習していく過程で go generate を使って
既存のORMモデルを元に、ORMモデルをdomainモデルに変換するマッパーを自動生成するようにしました。
フォルダ構成
クリーンアーキテクチャで構成しているため、必要もファイルのみ抜粋しています。
├── domains
│ ├── entity
│ │ ├── recipe.go # Domainモデル
│ │ ├── recipe_material.go # Domainモデル
└── repositories
├── model
│ ├── recipe_model.go # ORMモデル
│ └── recipe_material_model.go # ORMモデル
├── gen
│ ├── gen.go # generator
│ └── mappter.tmpl # 参照するテンプレート
元となるORMモデル
bunを使用して作成しています。
# repositories/rerecipe_model.go
package model
import (
"time"
"github.com/uptrace/bun"
)
// ORM モデル
type Recipe struct {
bun.BaseModel `bun:"table:recipes"`
Id int64 `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
Title string `bun:"title,notnull"`
Content string `bun:"content,notnull"`
Image string `bun:"image"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
RecipeMaterials []RecipeMaterial `bun:"rel:has-many,join:id=recipe_id"`
}
# repositories/rerecipe_material_model.go
package model
import (
"time"
"github.com/uptrace/bun"
)
// ORM モデル
type RecipeMaterial struct {
bun.BaseModel `bun:"table:recipe_materials"`
Id int64 `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
RecipeId int64 `bun:"recipe_id,notnull"`
Name string `bun:"name,notnull"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
}
generate のコード
今回は、recipe_model.go、recipe_material_model.go を元にmodelを生成するようにしています。
/repositories/gen/gen.go
//go:generate go run .
//go:generate gofmt -w ../
package main
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"log"
"os"
"strings"
"text/template"
)
// 構造体定義
type StructDef struct {
Name string
Fields []FieldDef
}
// 構造体フィールド定義
type FieldDef struct {
Name string
Type string
ArrayType string
}
// 配列のフィールド保存用
type FieldArray struct {
Name string
Type string
}
// ファイルをパースしてASTから必要な構造体情報を返却
func parseFirstStruct(fpath string) ([]StructDef, []FieldArray, error) {
// ファイルの中身をast構造にする
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, fpath, nil, parser.Mode(0))
if err != nil {
return nil, nil, err
}
list := []StructDef{}
farray := []FieldArray{}
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.TypeSpec:
sdef := StructDef{}
// 対象が構造体
st, ok := x.Type.(*ast.StructType)
if !ok {
return true
}
sdef.Name = x.Name.Name
for _, fld := range st.Fields.List {
// bun.BaseModel `bun:"table:recipes"` などはNamesがない
if fld.Names == nil {
continue
}
arrayType := ""
switch fldType := fld.Type.(type) {
case *ast.ArrayType:
var fldTypeBuf bytes.Buffer
err := printer.Fprint(&fldTypeBuf, fset, fldType.Elt)
if err != nil {
log.Fatalf("failed printing %s", err)
}
arrayType = fldTypeBuf.String()
farray = append(farray, FieldArray{Name: fld.Names[0].Name, Type: arrayType})
}
var typeNameBuf bytes.Buffer
err := printer.Fprint(&typeNameBuf, fset, fld.Type)
if err != nil {
log.Fatalf("failed printing %s", err)
}
sdef.Fields = append(
sdef.Fields,
FieldDef{Name: fld.Names[0].Name,
Type: typeNameBuf.String(),
ArrayType: arrayType,
})
}
list = append(list, sdef)
}
return true
})
return list, farray, nil
}
// マッパーファイルを生成
func createMapperFile(outputFilePath string, def StructDef, fa []FieldArray) error {
file, err := os.Create(outputFilePath)
if err != nil {
return err
}
defer file.Close()
t := template.Must(template.ParseFiles("./mappter.tmpl"))
data := map[string]interface{}{
"ModelName": def.Name, // "Comment"
"Fields": def.Fields,
"ArrayFields": fa,
}
if err := t.Execute(file, data); err != nil {
return err
}
fmt.Println(outputFilePath + " is generated.")
return nil
}
// エントリーポイント
func main() {
inputFilePaths := []string{"../recipe_model.go", "../recipe_material_model.go"}
for _, filepath := range inputFilePaths {
list, farray, err := parseFirstStruct(filepath)
if err != nil || len(list) == 0 {
fmt.Fprintf(os.Stderr, "model parse faild.\n: %s", err)
os.Exit(1)
}
outputFilePath := "../" + strings.Split(strings.Split(filepath, "/")[1], ".")[0] + "_mapper.gen.go"
err = createMapperFile(outputFilePath, list[0], farray)
if err != nil {
fmt.Fprintf(os.Stderr, "code generate failed.\n: %s", err)
os.Exit(1)
}
}
}
generate実行
/repositories/model/gen # go generate .
../recipe_model_mapper.gen.go is generated.
../recipe_material_model_mapper.gen.go is generated.
/repositories/gen/mapper.tmpl
// Code generated by go generate DO NOT EDIT.
package model
import "github.com/react/next-sample/backend/domain/entity"
// ORM モデルを Entity に変換
func (db{{ .ModelName }} *{{ .ModelName }}) ToEntity() *entity.{{ .ModelName }} {
{{range .ArrayFields -}}
entity{{ .Type }} := make([]entity.{{ .Type }}, len(db{{ $.ModelName }}.{{ .Name }}))
for i, tmpData := range db{{ $.ModelName }}.{{ .Name }} {
entity{{ .Type }}[i] = *tmpData.ToEntity()
}
{{end -}}
return &entity.{{ .ModelName }}{
{{range .Fields -}}
{{ .Name }}:
{{if ne .ArrayType "" -}}
entity{{ .ArrayType -}}
{{ else -}}
db{{ $.ModelName }}.{{ .Name -}}
{{ end -}},
{{end -}}
}
}
func To{{ .ModelName }}Mapper(entity *entity.{{ .ModelName }}) *{{ .ModelName }} {
return &{{ .ModelName }}{
{{range .Fields -}}
{{if ne .ArrayType "" -}}
{{ else -}}
{{ .Name }}: entity.{{ .Name }},
{{end -}}
{{end -}}
}
}
自動生成されたモデル
/repositories/model/recipe_model_mapper.gen.go
// Code generated by go generate DO NOT EDIT.
package model
import "github.com/react/next-sample/backend/domain/entity"
// ORM モデルを Entity に変換
func (dbRecipe *Recipe) ToEntity() *entity.Recipe {
entityRecipeMaterial := make([]entity.RecipeMaterial, len(dbRecipe.RecipeMaterials))
for i, tmpData := range dbRecipe.RecipeMaterials {
entityRecipeMaterial[i] = *tmpData.ToEntity()
}
return &entity.Recipe{
Id: dbRecipe.Id,
Title: dbRecipe.Title,
Content: dbRecipe.Content,
Image: dbRecipe.Image,
CreatedAt: dbRecipe.CreatedAt,
UpdatedAt: dbRecipe.UpdatedAt,
RecipeMaterials: entityRecipeMaterial,
}
}
func ToRecipeMapper(entity *entity.Recipe) *Recipe {
return &Recipe{
Id: entity.Id,
Title: entity.Title,
Content: entity.Content,
Image: entity.Image,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
/repositories/model/recipe_material_model_mapper.gen.go
// Code generated by go generate DO NOT EDIT.
package model
import "github.com/react/next-sample/backend/domain/entity"
// ORM モデルを Entity に変換
func (dbRecipeMaterial *RecipeMaterial) ToEntity() *entity.RecipeMaterial {
return &entity.RecipeMaterial{
Id: dbRecipeMaterial.Id,
RecipeId: dbRecipeMaterial.RecipeId,
Name: dbRecipeMaterial.Name,
CreatedAt: dbRecipeMaterial.CreatedAt,
UpdatedAt: dbRecipeMaterial.UpdatedAt,
}
}
func ToRecipeMaterialMapper(entity *entity.RecipeMaterial) *RecipeMaterial {
return &RecipeMaterial{
Id: entity.Id,
RecipeId: entity.RecipeId,
Name: entity.Name,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
参考サイト