1. モデルとフィールドの基礎

1.1. モデル定義の概要

ORMフレームワークでは、モデルはアプリケーション内のエンティティタイプとデータベーステーブルのマッピング関係を記述するために使用されます。モデルは、エンティティのプロパティや関係、およびそれらに関連するデータベース固有の構成を定義します。entフレームワークでは、モデルは通常、UserGroupなどのエンティティタイプをグラフで記述するために使用されます。

モデル定義には、通常、エンティティのフィールド(またはプロパティ)やエッジ(または関係)、およびいくつかのデータベース固有のオプションの説明が含まれます。これらの記述は、エンティティの構造、プロパティ、関係を定義するのに役立ち、モデルに基づいて対応するデータベーステーブルの構造を生成するために使用されます。

1.2. フィールドの概要

フィールドは、エンティティのプロパティを表すモデルの一部です。これらは、名前、年齢、日付などのエンティティのプロパティを定義します。entフレームワークでは、フィールドの種類には、整数、文字列、ブール値、時刻などのいくつかの基本データ型、およびUUID、[]byte、JSONなどのいくつかのSQL固有の型が含まれます。

以下の表は、entフレームワークでサポートされるフィールドの種類を示しています。

種類 説明
int 整数型
uint8 符号なし8ビット整数型
float64 浮動小数点型
bool ブール型
string 文字列型
time.Time 時刻型
UUID UUID型
[]byte バイト配列型(SQL専用)
JSON JSON型(SQL専用)
Enum Enum型(SQL専用)
その他 その他の型(例:Postgres Range)

2. フィールドプロパティの詳細

2.1. データ型

エンティティモデル内の属性またはフィールドのデータ型は、格納できるデータの形式を決定します。これはentフレームワークにおけるモデル定義の重要な部分です。以下は、entフレームワークでよく使用されるデータ型のいくつかです。

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.Bool("active"),         // ブール型
        field.Float("score"),         // 浮動小数点型
        field.Time("created_at"),     // タイムスタンプ型
    }
}
  • int: 整数値を表し、int8int16int32int64などになります。
  • string: 文字列データを表します。
  • bool: ブール値を表し、通常はフラグとして使用されます。
  • float64: 浮動小数点数を表し、float32も使用できます。
  • time.Time: 時間を表し、通常はタイムスタンプや日付データに使用されます。

これらのフィールド型は、基礎となるデータベースでサポートされる対応する型にマッピングされます。さらに、entUUIDJSONなどのより複雑な型、Enumなどの列挙型、および[]byte(SQL専用)やOther(SQL専用)などの特殊なデータベース型もサポートしています。

2.2. デフォルト値

フィールドにはデフォルト値を設定することができます。エンティティを作成する際に対応する値が指定されていない場合、デフォルト値が使用されます。デフォルト値は固定値または関数から動的に生成される値にすることができます。.Defaultメソッドを使用して静的なデフォルト値を設定するか、.DefaultFuncを使用して動的に生成されるデフォルト値を設定します。

// Userスキーマ。
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Time("created_at").
            Default(time.Now),  // time.Nowの固定のデフォルト値
        field.String("role").
            Default("user"),   // 固定の文字列値
        field.Float("score").
            DefaultFunc(func() float64 {
                return 10.0  // 関数によって生成されるデフォルト値
            }),
    }
}

2.3. フィールドのオプショナル設定とゼロ値

デフォルトでは、フィールドは必須です。オプショナルなフィールドを宣言するには、.Optional() メソッドを使用します。オプショナルなフィールドは、データベース内で null 許容のフィールドとして宣言されます。Nillable オプションを使用すると、フィールドを明示的に nil に設定することができ、フィールドのゼロ値と未設定の状態を区別することができます。

// ユーザースキーマ
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // オプショナルなフィールドは必須ではありません
        field.Int("age").Optional().Nillable(), // Nillable フィールドは、nil に設定できます
    }
}

上記で定義したモデルを使用する場合、age フィールドは、未設定を示すために nil 値、および非 nil のゼロ値の両方を受け入れることができます。

2.4. フィールドの一意性

一意制約を持つフィールドは、データベーステーブル内で重複する値がないことを保証します。一意なフィールドを定義するには、Unique() メソッドを使用します。ユーザーのメールアドレスやユーザー名など、データ整合性を重要視する場合には、一意なフィールドを使用するべきです。

// ユーザースキーマ
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // 重複するメールアドレスを避けるための一意なフィールド
    }
}

これにより、データベース内に重複する値を挿入することを防ぐための一意制約が作成されます。

2.5. フィールドのインデックス

フィールドのインデックス設定は、特に大規模なデータベースでデータベースクエリのパフォーマンスを向上させるために使用されます。ent フレームワークでは、.Indexes() メソッドを使用してインデックスを作成できます。

import "entgo.io/ent/schema/index"

// ユーザースキーマ
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // 'email' フィールドにインデックスを作成
        index.Fields("name", "age").Unique(), // 一意な複合インデックスを作成
    }
}

インデックスは頻繁にクエリされるフィールドに利用することができますが、インデックスを多く作成すると書き込み操作のパフォーマンスが低下する可能性があります。そのため、実際の状況に基づいてインデックスを作成するかどうかの判断はバランスを取る必要があります。

2.6. カスタムタグ

ent フレームワークでは、生成されたエンティティの構造体フィールドにカスタムタグを追加するために StructTag メソッドを使用できます。これらのタグは、JSON エンコーディングや XML エンコーディングなどの操作を実装するために非常に役立ちます。以下の例では、name フィールドにカスタムの JSON タグと XML タグを追加します。

// ユーザーのフィールド
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // StructTag メソッドを使用してカスタムタグを追加
            // ここでは、name フィールドの JSON タグを 'username' に設定し、フィールドが空の場合には無視します(omitempty)
            // また、エンコーディング用の XML タグを 'name' に設定します
            StructTag(`json:"username,omitempty" xml:"name"`),
    }
}

JSON や XML でエンコーディングする際、omitempty オプションは、name フィールドが空の場合には、そのフィールドをエンコーディング結果から省略することを示します。これは、APIを作成する際にレスポンスボディのサイズを削減するために非常に便利です。

また、同じフィールドに複数のタグを同時に設定する方法も示しています。JSON タグには json キー、XML タグには xml キーを使用し、それらはスペースで区切られています。これらのタグは、encoding/jsonencoding/xml などのライブラリ関数が、構造体のエンコーディングやデコーディングの際に使用されます。

3.1. 組み込みバリデータ

このフレームワークでは、さまざまな種類のフィールドに対する一般的なデータ妥当性チェックを実行するための組み込みバリデータが提供されています。これらの組み込みバリデータを使用することで、開発プロセスが簡素化され、フィールドの妥当なデータ範囲や形式を迅速に定義することができます。

以下は、組み込みのフィールドバリデータの例です:

  • 数値型のバリデータ:
    • Positive(): フィールドの値が正の数であるかを検証します。
    • Negative(): フィールドの値が負の数であるかを検証します。
    • NonNegative(): フィールドの値が非負の数であるかを検証します。
    • Min(i): フィールドの値が与えられた最小値 i より大きいかを検証します。
    • Max(i): フィールドの値が与えられた最大値 i より小さいかを検証します。
  • string 型のバリデータ:
    • MinLen(i): 文字列の最小長を検証します。
    • MaxLen(i): 文字列の最大長を検証します。
    • Match(regexp.Regexp): 文字列が指定された正規表現と一致するかを検証します。
    • NotEmpty: 文字列が空でないかを検証します。

具体的なコード例を見てみましょう。この例では、User モデルが作成されており、非負の整数型の age フィールドと特定の形式の email フィールドが含まれています:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("email").
            Match(regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)),
    }
}

3.2. カスタムバリデータ

組み込みのバリデータは多くの一般的なバリデーション要件を処理できますが、時にはより複雑なバリデーションロジックが必要になることがあります。そのような場合には、特定のビジネスルールに準拠したカスタムバリデータを記述することができます。

カスタムバリデータは、フィールドの値を受け取り error を返す関数です。返された error が空でない場合、それはバリデーションの失敗を示します。カスタムバリデータの一般的な形式は次のとおりです:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // 電話番号が期待される形式に合致しているかを検証
                matched, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !matched {
                    return errors.New("電話番号の形式が間違っています")
                }
                return nil
            }),
    }
}

上記の例では、電話番号の形式を検証するカスタムバリデータを作成しました。

3.3. 制約

制約とは、データベースオブジェクトに特定のルールを強制するルールです。これらは、無効なデータの入力を防いだり、データの整合性を定義したりするために使用できます。

一般的なデータベース制約には次のようなものがあります:

  • 主キー制約: テーブル内の各レコードが一意であることを保証します。
  • ユニーク制約: カラムの値または複数のカラムの組み合わせがテーブル内で一意であることを保証します。
  • 外部キー制約: テーブル間の関係を定義し、参照整合性を確保します。
  • チェック制約: フィールドの値が特定の条件を満たしていることを保証します。

エンティティモデルでは、次のように制約を定義してデータの整合性を維持することができます:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // ユニーク制約を設定し、テーブル内でユーザ名が一意であることを保証します。
        field.String("email").
            Unique(), // ユニーク制約を設定し、テーブル内でメールアドレスが一意であることを保証します。
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // 外部キー制約で、他のユーザとの間に一意のエッジ関係を作成します。
    }
}

まとめると、フィールドのバリデーションと制約は、データの品質を確保し、予期しないデータエラーを回避するために重要です。ent フレームワークが提供するツールを活用することで、このプロセスをより簡単かつ信頼性の高いものにすることができます。