メインコンテンツへスキップする

フィールド

簡単な概要#

スキーマ内のフィールド(またはプロパティ) はノードに属します。 例: Useragenameusernamecreated_atの4つのフィールドを持っています。

re-fields-properties

フィールドはスキーマのFieldsメソッドによって返されます。 例:

package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// User スキーマ
type User struct {
ent.Schema
}
// 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のすべての数値型。 int, uint8, float64など。
  • bool
  • string
  • time.Time
  • UUID
  • []byte (SQL only).
  • JSON (SQL only).
  • Enum (SQL only).
  • Other (SQLのみ)
package schema
import (
"time"
"net/url"
"github.com/google/uuid"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// User スキーマ
type User struct {
ent.Schema
}
// 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型が使用され(codegen optionで変更できます)、データベース内部で自動加算(auto increment)されます。

idフィールドを全てのテーブルにおいて固有なものにしたい場合はマイグレーション時にWithGlobalUniqueIDオプション を使用してください。

もしidフィールドに異なる設定が必要な場合、あるいはidの値がエンティティ作成時にアプリケーションから与えられるようにしたい場合(UUIDを使用する場合など)は埋め込まれているidの設定を上書きしてください。 例:

// Groupのフィールド
func (Group) Fields() []ent.Field {
return []ent.Field{
field.Int("id").
StructTag(`json:"oid,omitempty"`),
}
}
// 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メソッドを使用してください。設定された関数はリソース作成時に呼び出されます。 より詳細な情報はこのFAQを参照してください。

// 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スキーマ
type Card struct {
ent.Schema
}
// Cardのフィールド
func (Card) Fields() []ent.Field {
return []ent.Field{
field.Float("amount").
SchemaType(map[string]string{
dialect.MySQL: "decimal(6,2)", // MySQL用の変換を上書き
dialect.Postgres: "numeric", // Postgres用の変換を上書き
}),
}
}

Goの型#

各フィールドに対応するGoの型はデフォルトでは基本的な型(組み込み型や標準パッケージの型)が設定されています。 例としてstringフィールドにはstring型が、timeフィールドにはtime.Time型が設定されています。 デフォルトで設定されている型を自分で定義した型などで上書きしたい場合はGoTypeメソッドを使用してください。

上書きする型は次の2つの条件どちらかを満たす必要があります。i. Goの基本的な型に変換できること。ii. ValueScannerインターフェースを実装していること。

package schema
import (
"database/sql"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"
"github.com/shopspring/decimal"
)
// Amountは、基本的なfloat64型に変換可能なGoの独自型です
type Amount float64
// Cardスキーマ
type Card struct {
ent.Schema
}
// Cardのフィールド
func (Card) Fields() []ent.Field {
return []ent.Field{
field.Float("amount").
GoType(Amount(0)),
field.String("name").
Optional().
// ValueScanner を実装している型
GoType(&sql.NullString{}),
field.Enum("role").
// string に変換可能な型
GoType(role.Role("")),
field.Float("decimal").
// SchemaTypeと混合されたValueScanner 型
GoType(decimal.Decimal{}).
SchemaType(map[string]string{
dialect.MySQL: "decimal(6,2)",
dialect.Postgres: "numeric",
}),
}
}

Otherフィールド#

Otherフィールドは標準で提供されているフィールドでは適切に表現できない場合に使用します。 具体的にはPostgresのRange型やGeospatial型などです。

package schema
import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"
"github.com/jackc/pgtype"
)
// Userスキーマ
type User struct {
ent.Schema
}
// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.Other("duration", &pgtype.Tstzrange{}).
SchemaType(map[string]string{
dialect.Postgres: "tstzrange",
}),
}
}

デフォルト値#

DefaultメソッドやUpdateDefaultメソッドを使ってユニークでないフィールドに対してデフォルト値を設定できます。 また、DefaultFuncメソッドを使うことでデフォルト値のジェネレイターを設定することができます。

// 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固有の式をデフォルト値の設定に追加することができます。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
// CURRENT_TIMESTAMPをデフォルト値とする
// 新しいフィールドを追加
field.Time("created_at").
Default(time.Now).
Annotations(&entsql.Annotation{
Default: "CURRENT_TIMESTAMP",
}),
}
}

もしあなたのDefaultFuncがエラーも返す場合はschema-hooksを使って適切に処理したほうがよいでしょう。 詳しい情報はこのFAQを参照してください。

バリデーター#

フィールドのバリデーターはValidateメソッドを使って定義されるfunc(T) error型の関数で、エンティティ作成・更新時にそのフィールドの値に対して呼び出されます。

フィールドバリデーターがサポートしている型はstring型と全ての数値型です。

package schema
import (
"errors"
"regexp"
"strings"
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Groupスキーマ
type Group struct {
ent.Schema
}
// 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("グループは必ず大文字で始まる必要があります")
}
return nil
}),
}
}

再利用可能なバリデーターはこのように書きます。

import (
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/field"
)
// MaxRuneCount unicode/utf8 パッケージを使って rune 単位での長さを検証します。
func MaxRuneCount(maxLen int) func(s string) error {
return func(s string) error {
if utf8.RuneCountInString(s) > maxLen {
return errors.New("この文字列は長すぎます")
}
return nil
}
}
field.String("name").
// もし SQLデータベースを使用する場合は基本的なデータ型は varchar(10) に変更します。
Annotations(entsql.Annotation{
Size: 10,
}).
Validate(MaxRuneCount(10))
field.String("nickname").
// もし SQLデータベースを使用する場合は基本的なデータ型は varchar(20) に変更します。
Annotations(entsql.Annotation{
Size: 20,
}).
Validate(MaxRuneCount(20))

組み込みバリデーター#

このフレームワークは各型に対していくつかの組み込みバリデーターを提供しています。

  • 数値型

    • Positive()
    • Negative()
    • NonNegative()
    • Min(i) - 値が i より大きいか検証します(v > i)。
    • Max(i) - 値が i より小さいか検証します(v < i)。
    • Range(i, j) - 値が i 以上 j 以下か検証します(i <= v <= j)。
  • string型

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

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

オプショナル#

オプショナルなフィールドはエンティティの作成に必須ではないフィールドのことで、データベースではnullableなフィールドとして設定されます。 エッジとは異なりフィールドはデフォルトで必須となっています。フィールドを必須ではないものにするには明示的にOptionalメソッドを呼び出すべきです。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("required_name"),
field.String("optional_name").
Optional(),
}
}

Nillable#

データベースのカラムに0NULLが含まれている場合など、フィールドのゼロ値とnilを区別できるようにしたい場合があります。 Nillableオプションを使えばそれをすることができます。

T型のOptionalなフィールドにNillableオプションを設定すると生成される構造体のフィールドの型は*T型になります。 したがって、データベースがNULLをこのフィールドに返す場合、構造体のフィールドの値はnilになります。 そうでなければ、実際のデータへのポインタが含まれます。

例えばこのスキーマを指定すると

// 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エンティティ
type User struct {
RequiredName string `json:"required_name,omitempty"`
OptionalName string `json:"optional_name,omitempty"`
NillableName *string `json:"nillable_name,omitempty"`
}

イミュータブル#

不変なフィールドとは、エンティティの作成時にのみ設定できるフィールドのことです。 つまり、更新のためのセッターが生成されないということです。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}

ユニークネス(一意性)#

Uniqueメソッドを使うことによってフィールドを一意なものとして定義することができます。 一意なフィールドはデフォルト値を持つことが出来ないことに注意してください。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.String("nickname").
Unique(),
}
}

ストレージキー#

StorageKeyメソッドを使用することでカスタムストレージ名を設定できます。 これはSQLのカラム名やGremlinのプロパティ名にマッピングされます。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
StorageKey("old_name"),
}
}

インデックス#

インデックスは複数のフィールドや一部のエッジにも定義することができます。 これはSQLでのみ使える機能です。

詳細はインデックスセクションを参照してください。

タグ#

StructTagメソッドを使うことで生成されるエンティティのフィールドにカスタムタグを付与することができます。 このタグが使用されなかった場合、あるいはjsonタグに対して付与しなかった場合はデフォルトでjsonタグを追加します。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
StructTag(`gqlgen:"gql_name"`),
}
}

データベースに保存されないフィールド#

通常entが生成するエンティティモデルのフィールドはschema.Fieldsメソッドで設定されます。 例えばこのようなスキーマがあるとします。

// Userスキーマ
type User struct {
ent.Schema
}
// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Optional().
Nillable(),
field.String("name").
StructTag(`gqlgen:"gql_name"`),
}
}

生成されるモデルは以下のようになります。

// UserはUserスキーマに対するエンティティモデルです
type User struct {
// Ageは"age"フィールドの値を保持します
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
StaticField string `json:"static,omitempty"`
{{- end }}
{{ end }}

生成されるモデルは以下のようになります。

// UserはUserスキーマに対するエンティティモデルです
type User struct {
// Ageは"age"フィールドの値を保持します。
Age *int `json:"age,omitempty"`
// Nameは"name"フィールドの値を保持します。
Name string `json:"name,omitempty" gqlgen:"gql_name"`
// テンプレートで定義されたStaticField
StaticField string `json:"static,omitempty"`
}

隠匿されるフィールド#

文字列のフィールドはSensitiveメソッドを使うことで隠匿されるフィールドとして定義できます。 隠匿されたフィールドはプリントされず、エンコード時には省略されます。(unexportedなフィールドとして定義されるわけではないので注意してください。)

隠匿されたフィールドはタグを持つことができないことにも注意してください。

// Userスキーマ
type User struct {
ent.Schema
}
// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("password").
Sensitive(),
}
}

列挙型のフィールド#

Enum ビルダーによって許容する値をリストにした列挙型のフィールドを作ることができます。

// 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インターフェイスを実装している必要があります。

自ら定義したGoの型にはEnumValuesインターフェースも必要で、これによって列挙型で許容している値をEntに伝えます。

次の例で、stringに変換できる自ら定義したGoの型でEnumフィールドを定義する方法がわかります。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
// stringに変換できる型
field.Enum("shape").
GoType(property.Shape("")),
}
}

EnumValuesインターフェースを実装します。

package property
type Shape string
const (
Triangle Shape = "TRIANGLE"
Circle Shape = "CIRCLE"
)
// ValuesはEnumフィールドで有効な値のリストを与えます。
func (Shape) Values() (kinds []string) {
for _, s := range []Shape{Triangle, Circle} {
kinds = append(kinds, string(s))
}
return
}

次の例で、stringに変換できないがValueScannerインターフェイスを実装した自ら定義するGoの型でEnumフィールドを定義する方法がわかります。

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
// 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はEnumフィールドで有効な値のリストを与えます。
func (Level) Values() []string {
return []string{Unknown.String(), Low.String(), High.String()}
}
// Value はDBにintから変換した文字列を与えます。
func (p Level) Value() (driver.Value, error) {
return p.String(), nil
}
// Scanは列挙型を定義した型にどう読み込むかをコードに伝えます。
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
}

すべてを組み合わせると、

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
field.Enum("size").
Values("big", "small"),
// stringに変換できる型
field.Enum("shape").
GoType(property.Shape("")),
// 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 rawの値にシリアライズできなければならないことに注意してください(struct、map、sliceなど)。

// Userのスキーマ
type User struct {
ent.Schema
}
// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("creation_date").
Annotations(entgql.Annotation{
OrderField: "CREATED_AT",
}),
}
}

アノテーションとテンプレートでのその使い方についてより詳しくはtemplate docを読んでください。

命名規則#

慣例として、フィールド名にはsnake_caseを使用します。 entが生成する構造体の対応するフィールドは、PascalCaseを使用するGoの慣習に従います。 PascalCaseが必要な場合には、StorageKeyStructTagメソッドで指定することができます。