代码生成过程分析

Schema抽象类型

首先需要关注俩个重要的点:Schema的抽象数据类型,Graph的抽象数据类型。这俩者分别存储了Schema的定义和Graph生成的定义。

Schema的抽象数据类型

type Schema struct {
	Name        string                 `json:"name,omitempty"`
	Config      ent.Config             `json:"config,omitempty"`
	Edges       []*Edge                `json:"edges,omitempty"`
	Fields      []*Field               `json:"fields,omitempty"`
	Indexes     []*Index               `json:"indexes,omitempty"`
	Hooks       []*Position            `json:"hooks,omitempty"`
	Policy      []*Position            `json:"policy,omitempty"`
	Annotations map[string]interface{} `json:"annotations,omitempty"`
}

Graph的抽象数据类型

type (
	// The Config holds the global codegen configuration to be
	// shared between all generated nodes.
	Config struct {
		// Schema 保存用户 entschema 的 Go 包路径。
		// For example, "<project>/ent/schema".
		Schema string

		// Target 定义保存生成代码的目标目录的文件路径。例如,“.projectent”。
		//
		// By default, 'ent generate ./ent/schema' uses './ent' as a
		// target directory.
		Target string

		// Package 定义了上面提到的目标目录的 Go 包路径。例如,“github.comorgprojectent”。
		//
		// By default, for schema package named "<project>/ent/schema",
		// 'ent generate' uses "<project>/ent" as a default package.
		Package string

		// Header allows users to provides an optional header signature for
		// the generated files. It defaults to the standard 'go generate'
		// format: '// Code generated by entc, DO NOT EDIT.'.
		Header string

		// Storage configuration for the codegen. Defaults to sql.
		Storage *Storage

		// IDType 指定 codegen 中 id 字段的类型。支持的类型是 string 和 int,这也是默认值。
		IDType *field.TypeInfo

		// Templates specifies a list of alternative templates to execute or
		// to override the default. If nil, the default template is used.
		//
		// Note that, additional templates are executed on the Graph object and
		// the execution output is stored in a file derived by the template name.
		Templates []*Template

		// Features defines a list of additional features to add to the codegen phase.
		// For example, the PrivacyFeature.
		Features []Feature

		// Hooks holds an optional list of Hooks to apply on the graph before/after the code-generation.
		Hooks []Hook

		// Annotations that are injected to the Config object can be accessed
		// globally in all templates. In order to access an annotation from a
		// graph template, do the following:
		//
		//	{{- with $.Annotations.GQL }}
		//		{{/* Annotation usage goes here. */}}
		//	{{- end }}
		//
		// For type templates, we access the Config field to access the global
		// annotations, and not the type-specific annotation.
		//
		//	{{- with $.Config.Annotations.GQL }}
		//		{{/* Annotation usage goes here. */}}
		//	{{- end }}
		//
		// 请注意,映射是从注释名称(例如“GQL”)到 JSON 解码对象。
		Annotations Annotations
	}

	// Graph holds the nodes/entities of the loaded graph schema. Note that, it doesn't
	// hold the edges of the graph. Instead, each Type holds the edges for other Types.
	Graph struct {
		*Config
		// Nodes are list of Go types that mapped to the types in the loaded schema.
		Nodes []*Type
		// Schemas holds the raw interfaces for the loaded schemas.
		Schemas []*load.Schema
	}
)

需要注意的是:

  • Schema数据类型的实例是通过解析Schema的定义得到的。
  • Graph数据类型的实例是通过处理Schema数据,以及结合内外部模板得到的。内部模板是预先内置的,外部模板是用户指定的。

基于模板的生成代码过程分析

大致的流程是这样的:

1. 在系统内部定义了要生成的文件的模板,模板的名称和路径等数据,内置在[]Template数组中,以在生成目标文件的时候调用。
2. 处理Schema数据。在对Schema数据经过一系列的校验、转换等操作后加载到Graph中。比如:将schema转为Graph的节点,转换节点的属性,转换节点的边,转换节点的索引等等。
3. 关键的一步,结合graph和模板,生成目标文件内容。生成的文件路径和文件内容是存在于assets数组中的。当然,在写文件之前,这些全部存在于内存中。
4. 将所有生成的文件落盘。

1. 内置模板

var (
	// Templates 保存图形正在生成的文件的模板信息。
	Templates = []TypeTemplate{
		{
			Name:   "create",
			Format: pkgf("%s_create.go"),
			ExtendPatterns: []string{
				"dialect/*/create/fields/additional/*",
				"dialect/*/create_bulk/fields/additional/*",
			},
		},
		{
			Name:   "update",
			Format: pkgf("%s_update.go"),
		},
		{
			Name:   "delete",
			Format: pkgf("%s_delete.go"),
		},
		{
			Name:   "query",
			Format: pkgf("%s_query.go"),
			ExtendPatterns: []string{
				"dialect/*/query/fields/additional/*",
			},
		},
		{
			Name:   "model",
			Format: pkgf("%s.go"),
		},
		{
			Name:   "where",
			Format: pkgf("%s/where.go"),
			ExtendPatterns: []string{
				"where/additional/*",
			},
		},
		{
			Name: "meta",
			Format: func(t *Type) string {
				return fmt.Sprintf("%[1]s/%[1]s.go", t.PackageDir())
			},
			ExtendPatterns: []string{
				"meta/additional/*",
			},
		},
	}
	// GraphTemplates保存应用于图表的模板。
	GraphTemplates = []GraphTemplate{
		{
			Name:   "base",
			Format: "ent.go",
		},
		{
			Name:   "client",
			Format: "client.go",
			ExtendPatterns: []string{
				"client/fields/additional/*",
				"dialect/*/query/fields/init/*",
			},
		},
		{
			Name:   "context",
			Format: "context.go",
		},
		{
			Name:   "tx",
			Format: "tx.go",
		},
		{
			Name:   "config",
			Format: "config.go",
			ExtendPatterns: []string{
				"dialect/*/config/*/*",
			},
		},
		{
			Name:   "mutation",
			Format: "mutation.go",
		},
		{
			Name:   "migrate",
			Format: "migrate/migrate.go",
			Skip:   func(g *Graph) bool { return !g.SupportMigrate() },
		},
		{
			Name:   "schema",
			Format: "migrate/schema.go",
			Skip:   func(g *Graph) bool { return !g.SupportMigrate() },
		},
		{
			Name:   "predicate",
			Format: "predicate/predicate.go",
		},
		{
			Name:   "hook",
			Format: "hook/hook.go",
		},
		{
			Name:   "privacy",
			Format: "privacy/privacy.go",
			Skip: func(g *Graph) bool {
				return !g.featureEnabled(FeaturePrivacy)
			},
		},
		{
			Name:   "entql",
			Format: "entql.go",
			Skip: func(g *Graph) bool {
				return !g.featureEnabled(FeatureEntQL)
			},
		},
		{
			Name:   "runtime/ent",
			Format: "runtime.go",
		},
		{
			Name:   "enttest",
			Format: "enttest/enttest.go",
		},
		{
			Name:   "runtime/pkg",
			Format: "runtime/runtime.go",
		},
	}

2. 初始化Graph

// NewGraph creates a new Graph for the code generation from the given schema definitions.
// It fails if one of the schemas is invalid.
//从给定的sschema定义中生成图
//如果任意一个图创建失败,则schema是不可用的
func NewGraph(c *Config, schemas ...*load.Schema) (g *Graph, err error) {
	defer catch(&err)
	//传入配置,schema初始化图
	g = &Graph{c, make([]*Type, 0, len(schemas)), schemas}
	//遍历shcema,添加为图的节点
	for i := range schemas {
		g.addNode(schemas[i])
	}
	//遍历schema添加边
	for i := range schemas {
		g.addEdges(schemas[i])
	}
	//处理每个节点的边 变得四种关系
	for _, t := range g.Nodes {
		check(resolve(t), "resolve %q relations", t.Name)
	}
	//为每个边创建外键
	for _, t := range g.Nodes {
		check(t.setupFKs(), "set %q foreign-keys", t.Name)
	}
	//遍历shcema。添加索引到图中
	for i := range schemas {
		g.addIndexes(schemas[i])
	}
	//为了房子冲突,为导入的包定义本地别名
	aliases(g)
	//执行一下默认的内容,比如进行id类型的设置
	g.defaults()
	return
}

3. 生成代码

生成代码的核心是运用了text/template包来完成的

//生成模板
b := bytes.NewBuffer(nil)
if err := templates.ExecuteTemplate(b, tmpl.Name, g); err != nil {
  return fmt.Errorf("execute template %q: %w", tmpl.Name, err)
}

4. 写文件,格式化

type (
	file struct {
		path    string
		content []byte
	}
	assets struct {
		//涉及的目录存在这里,在写文件之前,创建他们
		dirs  []string
		files []file
	}
)

// 在资产中写入文件和目录。
func (a assets) write() error {
	for _, dir := range a.dirs {
		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
			return fmt.Errorf("create dir %q: %w", dir, err)
		}
	}
	for _, file := range a.files {
		if err := os.WriteFile(file.path, file.content, 0644); err != nil {
			return fmt.Errorf("write file %q: %w", file.path, err)
		}
	}
	return nil
}

// format runs "goimports" on all assets.
func (a assets) format() error {
	for _, file := range a.files {
		path := file.path
		src, err := imports.Process(path, file.content, nil)
		if err != nil {
			return fmt.Errorf("format file %s: %w", path, err)
		}
		if err := os.WriteFile(path, src, 0644); err != nil {
			return fmt.Errorf("write file %s: %w", path, err)
		}
	}
	return nil
}