代码生成过程分析
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
}