代码生成过程分析

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 }