跳到主要内容

字段

概述#

Schema 中的字段(或属性)是节点的属性。 例如:Useragename, usernamecreated_at 4个字段。

re-fields-properties

用 schema 的 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 方法设置为可选字段。

类型#

目前框架支持以下数据类型:

  • Go中所有数值类型。 如 intuint8float64
  • bool 布尔型
  • string 字符串
  • time.Time 时间类型
  • UUID
  • []byte (仅限SQL)。
  • JSON(仅限SQL)。
  • Enum(仅限SQL)。
  • 其它类型 (仅限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 字段在所有表中唯一,可以在运行 schema 迁移时使用 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 方法来指定一个函数,每次ID将由此函数生成。 更多信息请参阅 相关常见问题

// 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 方言将Go类型为 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 方法提供了以自定义类型覆盖 默认类型的选项。

自定义类型必须是可转换为Go基本类型,或者实现 ValueScanner 接口的类型。

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

其它字段#

Other 代表一个不适合任何标准字段类型的字段。 示例为 Postgres 中的 Rage 类型或 Geospatial 类型

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

默认值#

非唯一 字段可使用 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.Annotation{
Default: "CURRENT_TIMESTAMP",
}),
}
}

为避免你指定的 DefaultFunc 方法也返回了一个错误,最好使用 schema-hooks 处理它。 更多信息请参阅 相关常见问题

校验器#

字段校验器是一个 func(T) error 类型的函数,定义在 schema 的 Validate 方法中,字段在创建或更新前会执行此方法。

支持 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 可选项#

可选字段为创建时非必须的字段,在数据库中被设置为 null。 和 edges 不同,字段默认都为必需字段,可通过 Optional 方法显示的设为可选字段。

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

Nillable 空值#

有时您希望能将 0 值和nil区别开来; 例如,数据库列中存0NULL. 那么 Nillable 就派上用场了。

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

例如,在这个 schema 中:

// 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"`
}

Immutable 不可变的#

不可变字段只能在创建实体时设置。 即:不会为实体生成任何更新方法。

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

唯一键#

字段可以使用 Unique 方法定义为唯一字段。 注意:唯一字段不能有默认值。

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

存储键名#

可以使用 StorageKey 方法自定义数据库中的字段名称。 在 SQL 中为字段名,在 Gremlin 中为属性名称。

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

索引#

索引可以在多字段和某些类型的 edges 上定义. 注意:目前只有 SQL 类型的数据库支持此功能。

更多请参阅 索引 章节。

结构体标记(tags)#

可以使用 StructTag 方法将自定义结构标签添加到生成的实体中。 注意:如果此选择未设置,或者未包含 json 标签,那么默认的 json 标签将被设置为字段名。

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

附加元数据字段#

默认情况下, entschema.Fields 方法配置的字段生成实体模型。 例如,给定此schema配置:

// 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"`
}

向生成的结构体中添加的字段是未在数据库中定义的, 使用 external templates。 例如:

{{ 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"`
}

敏感字段#

字符串(String)字段归为敏感信息可借助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"),
}
}

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

同时 EnumValues 接口需要自定义的Go类型告诉Ent被允许的枚举值有哪些。

以下示例展示了如何将Enum字段使用的自定义Go类型转换到string类型:

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

以下示例展示了如何使用自定义Go类型定义一个Enum字段 并且不转换到 string,但是它实现了ValueScanner 接口:

// 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 interface{}) 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用于在生成代码时将任意的元数据附加到字段对象。 模板扩展可以检索此元数据并直接使用。

注意元数据必须能序列化为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",
}),
}
}

模板章节了解更多关于注解及其使用的相关信息。

命名规范#

根据惯例字段的命名应使用snake_case(蛇形命名法)。 由ent生成相应的结构体字段使用是Go规范的PascalCase(帕斯卡命名法)。 在需要PascalCase 时,您可以借助StorageKeyStructTag方法实现。