跳到主要内容

字段(Fields)

快速摘要

模式中的字段(或属性)是节点的属性。例如:一个 User 具有四个字段 agenameusernamecreated_at

re-fields-properties

字段通过 Fields 方法从模式中返回。例如:

package schema

import (
"time"

"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age"),
field.String("name"),
field.String("username").
Unique(),
field.Time("created_at").
Default(time.Now),
}
}

所有字段默认均为必填项,可通过 Optional 方法将其设置为可选项。

类型(Types)

以下是框架当前支持的类型:

  • 所有的 Go 语言数字类型。例如 int, uint8, float64 等。
  • bool
  • string
  • time.Time
  • UUID
  • []byte (仅限SQL)
  • JSON (仅限SQL)
  • Enum (仅限SQL)
  • Other (仅限SQL)
package schema

import (
"time"
"net/url"

"github.com/google/uuid"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.Float("rank").
Optional(),
field.Bool("active").
Default(false),
field.String("name").
Unique(),
field.Time("created_at").
Default(time.Now),
field.JSON("url", &url.URL{}).
Optional(),
field.JSON("strings", []string{}).
Optional(),
field.Enum("state").
Values("on", "off").
Optional(),
field.UUID("uuid", uuid.UUID{}).
Default(uuid.New),
}
}

若需了解每种类型与数据库中的类型如何映射,可以参考 迁移 章节。

ID 字段

id 字段是模式内置的,无需特意声明。在基于SQL的数据库中默认类型是 int (可以通过 代码生成选项 修改 ) 且在数据库中自增。

为使 id 字段在所有表中都唯一,可以在运行模式迁移时使用 WithGlobalUniqueID 选项。

如果需要不同以上的 id 字段配置,或 id 的值需要由应用程序在创建实体时提供(例如 UUID),可以覆盖内置 id 配置,如下:

// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.Int("id").
StructTag(`json:"oid,omitempty"`),
}
}

// Fields of the Blob.
func (Blob) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Default(uuid.New).
StorageKey("oid"),
}
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("id").
MaxLen(25).
NotEmpty().
Unique().
Immutable(),
}
}

如果你需要设置自定义的函数来生成 ID,可以使用 DefaultFunc 来指定函数,这会在资源每次创建时运行。查阅 相关问题 获取更多信息。

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int64("id").
DefaultFunc(func() int64 {
// An example of a dumb ID generator - use a production-ready alternative instead.
return time.Now().Unix() << 8 | atomic.AddInt64(&counter, 1) % 256
}),
}
}

数据库类型

每种数据库方言都有自己由GO语言类型到数据库类型的映射。例如 MySQL 方言在数据库中将 float64 映射为 double 列。因此可以通过 SchemaType 方法覆盖默认行为。

package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"
)

// Card schema.
type Card struct {
ent.Schema
}

// Fields of the Card.
func (Card) Fields() []ent.Field {
return []ent.Field{
field.Float("amount").
SchemaType(map[string]string{
dialect.MySQL: "decimal(6,2)", // Override MySQL.
dialect.Postgres: "numeric", // Override Postgres.
}),
}
}

Go 语言类型

字段的默认类型都是 Go 语言基础类型。例如字符串字段类型是 string ,时间字段类型是 time.TimeGoType 方法可以将默认的 ent 类型覆盖为自定义类型。

自定义类型必须可以转化为 Go 语言基本类型,即实现了 ValueScanner 接口,或是一个 外部的 ValueScanner。此外,如果提供的类型实现了Validator接口且未设置任何验证器,则将使用该类型的验证器。

package schema

import (
"database/sql"

"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"
"github.com/shopspring/decimal"
)

// Amount is a custom Go type that's convertible to the basic float64 type.
type Amount float64

// Card schema.
type Card struct {
ent.Schema
}

// Fields of the Card.
func (Card) Fields() []ent.Field {
return []ent.Field{
field.Float("amount").
GoType(Amount(0)),
field.String("name").
Optional().
// A ValueScanner type.
GoType(&sql.NullString{}),
field.Enum("role").
// A convertible type to string.
GoType(role.Role("")),
field.Float("decimal").
// A ValueScanner type mixed with SchemaType.
GoType(decimal.Decimal{}).
SchemaType(map[string]string{
dialect.MySQL: "decimal(6,2)",
dialect.Postgres: "numeric",
}),
}
}

外部的 ValueScanner

Ent 允许为基础和自定义的 Go 语言类型添加自定义 ValueScanner。这使得在不实现 ValueScanner 接口的情况下,既能使用标准模式字段,又能控制它们在数据库中的存储方式。此外,此选项允许用户使用未实现 ValueScannerGoType,例如 *url.URL

备注

现阶段此选项只对文字和数据字段有效,其他类型在未来版本中将会扩展。

实现了 encoding.TextMarshallerencoding.TextUnmarshaller 接口的自定义 Go 语言类型可以使用 field.TextValueScanner 作为 ValueScannerValueScanner 调用 MarshalTextUnmarshalText 在数据库中读写字段值:

field.String("big_int").
GoType(&big.Int{}).
ValueScanner(field.TextValueScanner[*big.Int]{})

其他字段

其他表示不适合任何标准字段类型的字段。例如 Postgres 数据库的范围类型或地理空间类型。

package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"

"github.com/jackc/pgtype"
)

// User schema.
type User struct {
ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Other("duration", &pgtype.Tstzrange{}).
SchemaType(map[string]string{
dialect.Postgres: "tstzrange",
}),
}
}

默认值

Non-unique 字段使用 DefaultUpdateDefault 方法支持默认值。你也可以指明 DefaultFunc 替代其为自定义生成器。

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
field.String("name").
Default("unknown"),
field.String("cuid").
DefaultFunc(cuid.New),
field.JSON("dirs", []http.Dir{}).
Default([]http.Dir{"/tmp"}),
}
}

可通过 entsql.Annotation: 将 SQL 特定的字面量或表达式(如函数调用)添加到默认值配置中:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
// Add a new field with CURRENT_TIMESTAMP
// as a default value to all previous rows.
field.Time("created_at").
Default(time.Now).
Annotations(
entsql.Default("CURRENT_TIMESTAMP"),
),
// Add a new field with a default value
// expression that works on all dialects.
field.String("field").
Optional().
Annotations(
entsql.DefaultExpr("lower(other_field)"),
),
// Add a new field with custom default value
// expression for each dialect.
field.String("default_exprs").
Optional().
Annotations(
entsql.DefaultExprs(map[string]string{
dialect.MySQL: "TO_BASE64('ent')",
dialect.SQLite: "hex('ent')",
dialect.Postgres: "md5('ent')",
}),
),
}
}

考虑到你的 DefaultFunc 也会返回错误,最好使用 模式钩子 进行处理。 参阅 相关问题 获取更多信息。

验证器

字段验证器是在模式中使用 Validate 方法定义的 func(T) error 类型的函数,在创建或更新实体之前应用与字段的值的校验。

字段验证器支持的字段类型为 string 和所有的数值类型。

package schema

import (
"errors"
"regexp"
"strings"
"time"

"entgo.io/ent"
"entgo.io/ent/schema/field"
)


// Group schema.
type Group struct {
ent.Schema
}

// Fields of the group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Match(regexp.MustCompile("[a-zA-Z_]+$")).
Validate(func(s string) error {
if strings.ToLower(s) == s {
return errors.New("group name must begin with uppercase")
}
return nil
}),
}
}

这是另外一个可复用验证器的示例:

import (
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/field"
)

// MaxRuneCount validates the rune length of a string by using the unicode/utf8 package.
func MaxRuneCount(maxLen int) func(s string) error {
return func(s string) error {
if utf8.RuneCountInString(s) > maxLen {
return errors.New("value is more than the max length")
}
return nil
}
}

field.String("name").
// If using a SQL-database: change the underlying data type to varchar(10).
Annotations(entsql.Annotation{
Size: 10,
}).
Validate(MaxRuneCount(10))
field.String("nickname").
// If using a SQL-database: change the underlying data type to varchar(20).
Annotations(entsql.Annotation{
Size: 20,
}).
Validate(MaxRuneCount(20))

内置验证器

本框架为每种类型提供了少数内置验证器:

  • 数值类型:

    • Positive()
    • Negative()
    • NonNegative()
    • Min(i) - 验证值大于 i.
    • Max(i) - 验证值小于 i.
    • Range(i, j) - 验证值在 [i, j] 之间(闭区间).
  • string

    • MinLen(i)
    • MaxLen(i)
    • Match(regexp.Regexp)
    • NotEmpty
  • []byte

    • MaxLen(i)
    • MinLen(i)
    • NotEmpty

可选字段(Optional)

可选字段是指在实体创建过程中非必需的字段,在数据库中将被设置为可空字段。 与边不同,字段默认是必需的,若要将其设为可选,应显式使用 Optional 方法进行设置。

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("required_name"),
field.String("optional_name").
Optional(),
}
}

空字段(Nillable)

有时你希望能够区别字段零值和 nil。例如数据库列中包含 0NULLNillable 选项就是为解决这个问题。

如果你拥有一个 T 类型的 Optional 字段,将其设置为 Nillable 将会生成一个 *T 类型的字段结构。 因此如果数据库的此字段返回 NULL,这个字段就会是 nil。否则,它将包含指向实际值的指针。

例如以下模式:

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("required_name"),
field.String("optional_name").
Optional(),
field.String("nillable_name").
Optional().
Nillable(),
}
}

User 实体生成的结构如下:

ent/user.go
package ent

// User entity.
type User struct {
RequiredName string `json:"required_name,omitempty"`
OptionalName string `json:"optional_name,omitempty"`
NillableName *string `json:"nillable_name,omitempty"`
}

Nillable 必填字段

在 JSON 序列化时字段若查询时未选择或提供值,Nillable字段同样可以避免零值。 例如一个 time.Time 字段:

// Fields of the task.
func (Task) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.Time("nillable_created_at").
Default(time.Now).
Nillable(),
}
}

生成的 Task 实体结构如下:

ent/task.go
package ent

// Task entity.
type Task struct {
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// NillableCreatedAt holds the value of the "nillable_created_at" field.
NillableCreatedAt *time.Time `json:"nillable_created_at,omitempty"`
}

json.Marshal 结果是:

b, _ := json.Marshal(Task{})
fmt.Printf("%s\n", b)
// {"created_at":"0001-01-01T00:00:00Z"}

now := time.Now()
b, _ = json.Marshal(Task{CreatedAt: now, NillableCreatedAt: &now})
fmt.Printf("%s\n", b)
// {"created_at":"2009-11-10T23:00:00Z","nillable_created_at":"2009-11-10T23:00:00Z"}

不可变字段(Immutable)

不可变字段是指仅能在实体创建时设置的字段。也就是说,实体的更新构建器不会为这些字段生成设置器。

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}

唯一值(Uniqueness)

字段可通过 Unique 方法定义为唯一。请注意,唯一字段不能设置默认值。

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("nickname").
Unique(),
}
}

评论(Comments)

可以通过使用 .Comment() 方法将评论添加到字段。评论在生成实体代码出现在字段之前。换行符通过转义序列 \n 实现支持。

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Default("John Doe").
Comment("Name of the user.\n If not specified, defaults to \"John Doe\"."),
}
}

已弃用字段(Deprecated Fields)

Deprecated 方法标识某个字段已被弃用。弃用字段默认在查询中不会被选则,并且在生成的代码中字段结构声明为 Deprecated

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Deprecated("use `full_name` instead"),
}
}

存储名称(Storage Key)

可以通过 StorageKey 方法配置定义的存储名称。这会将其映射为SQL方言中的列名以及 Gremlin 中的属性名。

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
StorageKey("old_name"),
}
}

索引(Indexes)

索引可以定义在多个字段和某些类型的边。但请注意,此功能当前仅限于 SQL 功能。

更多请参阅 索引 部分。

结构体标签(Struct Tags)

可以使用 StructTag 将结构体标签添加到生成的实体。注意如果未提供此选项或选项中没有包含 json 标签,默认的具有字段名称的 json 标签会被添加。

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
StructTag(`gqlgen:"gql_name"`),
}
}

额外结构体字段

默认情况下, ent 通过 schema.Fields 方法配置生成带有字段的实体模型。 例如给定以下模式配置:

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Optional().
Nillable(),
field.String("name").
StructTag(`gqlgen:"gql_name"`),
}
}

生成如下模式:

// User is the model entity for the User schema.
type User struct {
// Age holds the value of the "age" field.
Age *int `json:"age,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty" gqlgen:"gql_name"`
}

为将额外的字段添加到生成的结构体中但是 不在数据库中存储 ,可以使用 外部模板,例如:

{{ define "model/fields/additional" }}
{{- if eq $.Name "User" }}
// StaticField defined by template.
StaticField string `json:"static,omitempty"`
{{- end }}
{{ end }}

生成如下模型:

// User is the model entity for the User schema.
type User struct {
// Age holds the value of the "age" field.
Age *int `json:"age,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty" gqlgen:"gql_name"`
// StaticField defined by template.
StaticField string `json:"static,omitempty"`
}

敏感字段

字符串字段可以通过 Sensitive 方法定义为敏感字段。敏感字段不会被打印且在编码时会被忽略。

注意敏感字段不能有结构体标签。

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("password").
Sensitive(),
}
}

枚举字段

Enum 构建器允许创建允许值列表的枚举字段。

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
field.Enum("size").
Values("big", "small"),
}
}

默认情况下,Ent 使用简单的字符串类型来表示 PostgreSQL 和 SQLite 中的枚举值。但在某些情况下,您可能希望使用数据库提供的原生枚举类型。更多信息请参阅 枚举迁移指南

当使用自定义 GoType 时,必须能转化为基础的 string 类型或实现 ValueScanner 接口。

自定义 Go 语言类型同样要求实现 EnumValues 接口,以便告诉 Ent 给定的枚举值。

下面的示例展示了如何通过自定义可转为 string 的 Go 语言类型定义 Enum 字段。

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
// A convertible type to string.
field.Enum("shape").
GoType(property.Shape("")),
}
}

实现 EnumValues 接口。

package property

type Shape string

const (
Triangle Shape = "TRIANGLE"
Circle Shape = "CIRCLE"
)

// Values provides list valid values for Enum.
func (Shape) Values() (kinds []string) {
for _, s := range []Shape{Triangle, Circle} {
kinds = append(kinds, string(s))
}
return
}

下面的示例展示了如何通过无法转为 string 类型但是实现了 ValueScanner 接口的自定义 Go 语言类型定义 Enum 字段:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
// Add conversion to and from string
field.Enum("level").
GoType(property.Level(0)),
}
}

同样实现 ValueScanner 接口。

package property

import "database/sql/driver"

type Level int

const (
Unknown Level = iota
Low
High
)

func (p Level) String() string {
switch p {
case Low:
return "LOW"
case High:
return "HIGH"
default:
return "UNKNOWN"
}
}

// Values provides list valid values for Enum.
func (Level) Values() []string {
return []string{Unknown.String(), Low.String(), High.String()}
}

// Value provides the DB a string from int.
func (p Level) Value() (driver.Value, error) {
return p.String(), nil
}

// Scan tells our code how to read the enum into our type.
func (p *Level) Scan(val any) error {
var s string
switch v := val.(type) {
case nil:
return nil
case string:
s = v
case []uint8:
s = string(v)
}
switch s {
case "LOW":
*p = Low
case "HIGH":
*p = High
default:
*p = Unknown
}
return nil
}

将他们结合到一起:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
field.Enum("size").
Values("big", "small"),
// A convertible type to string.
field.Enum("shape").
GoType(property.Shape("")),
// Add conversion to and from string.
field.Enum("level").
GoType(property.Level(0)),
}
}

生成代码后的使用很简单:

client.User.Create().
SetFirstName("John").
SetLastName("Dow").
SetSize(user.SizeSmall).
SetShape(property.Triangle).
SetLevel(property.Low).
SaveX(context.Background())

john := client.User.Query().FirstX(context.Background())
fmt.Println(john)
// User(id=1, first_name=John, last_name=Dow, size=small, shape=TRIANGLE, level=LOW)

注解(Annotations)

Annotations 用于在代码生成过程中向字段对象附加任意元数据。模板扩展可以检索这些元数据并在其模板内部使用。

注意元数据对象必须能被序列化为原始 JSON 值(例如 struct、map 或 slice)。

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("creation_date").
Annotations(entgql.Annotation{
OrderField: "CREATED_AT",
}),
}
}

参阅 [模板文档] (templates.md#annotations) 了解更多注解以及他们在模板中的使用。

命名规范

按惯例,字段名称应采用蛇形命名法 snake_case。由 ent 生成的对应结构体字段将遵循 Go 语言惯例,采用驼峰命名法 PascalCase。若需使用驼峰命名法 PascalCase ,可通过 StorageKeyStructTag 方法实现。