0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

go generateでORMモデル→domainモデルのmapperを自動生成

Posted at

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,
	}
}


参考サイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?