1. entの紹介

Ent(エンティティ)は、FacebookによってGo言語向けに開発されたエンティティフレームワークです。これは、大規模なデータモデルアプリケーションの構築とメンテナンスを簡素化することを目的としています。entフレームワークは主に次の原則に従います:

  • データベーススキーマをグラフ構造として簡単にモデル化する。
  • Go言語のコード形式でスキーマを定義する。
  • コード生成に基づく静的な型を実装する。
  • データベースクエリとグラフ探索を非常に簡単に行うことができる。
  • Goテンプレートを使用して簡単に拡張およびカスタマイズする。

2. 環境のセットアップ

entフレームワークを利用するには、開発環境にGo言語がインストールされていることを確認してください。

プロジェクトディレクトリがGOPATHの外にある場合や、GOPATHに慣れていない場合は、次のコマンドを使用して新しいGoモジュールプロジェクトを作成できます:

go mod init entdemo

これにより、新しいGoモジュールが初期化され、entdemoプロジェクトのための新しいgo.modファイルが作成されます。

3. 最初のスキーマの定義

3.1. ent CLIを使用したスキーマの作成

まず、ent CLIツールを使用してプロジェクトのルートディレクトリで次のコマンドを実行して、Userという名前のスキーマを作成する必要があります:

go run -mod=mod entgo.io/ent/cmd/ent new User

上記のコマンドは、entdemo/ent/schema/ディレクトリにUserスキーマを生成します:

ファイル entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

// UserはUserエンティティのスキーマ定義を保持します。
type User struct {
    ent.Schema
}

// Userのフィールド。
func (User) Fields() []ent.Field {
    return nil
}

// Userのエッジ。
func (User) Edges() []ent.Edge {
    return nil
}

3.2. フィールドの追加

次に、Userスキーマにフィールド定義を追加する必要があります。以下は、Userエンティティに2つのフィールドを追加する例です。

修正されたファイル entdemo/ent/schema/user.go:

package schema

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

// Userのフィールド。
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

このコードは、Userモデルの2つのフィールドを定義します:ageは正の整数であり、nameはデフォルト値が「unknown」の文字列です。

3.3. データベースエンティティの生成

スキーマを定義した後、前述のスキーマに基づいて対応するデータベースアクセスロジックを生成するために、go generateコマンドを実行する必要があります。

プロジェクトのルートディレクトリで次のコマンドを実行します:

go generate ./ent

このコマンドにより、前述のスキーマに基づいて対応するGoコードが生成され、以下のファイル構造が生成されます:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
...(省略されているファイルが複数あります)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. データベース接続の初期化

MySQLデータベースに接続するために、entフレームワークが提供するOpen関数を使用します。まず、MySQLドライバをインポートし、正しい接続文字列を提供してデータベース接続を初期化します。

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // MySQLドライバをインポート
)

func main() {
    // ent.Openを使用してMySQLデータベースとの接続を確立します。
    // 下記のプレースホルダー "your_username", "your_password", "your_database" を置き換えることを忘れないでください。
    client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
    if err != nil {
        log.Fatalf("mysqlへの接続のオープンに失敗しました: %v", err)
    }
    defer client.Close()

    // 自動マイグレーションツールを実行する
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("スキーマリソースの作成に失敗しました: %v", err)
    }
    
    // ここに追加のビジネスロジックを記述できます
}

4.2. エンティティの作成

ユーザーエンティティを作成するには、新しいエンティティオブジェクトを構築し、SaveまたはSaveXメソッドを使用してデータベースに永続化します。以下のコードは、新しいユーザーエンティティを作成し、agenameの2つのフィールドを初期化する方法を示しています。

// CreateUser関数は新しいUserエンティティを作成するために使用されます
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // client.User.Create()を使用してUserの作成リクエストを構築し、
    // その後、SetAgeおよびSetNameメソッドをチェーンしてエンティティフィールドの値を設定します。
    u, err := client.User.
        Create().
        SetAge(30).    // ユーザーの年齢を設定
        SetName("a8m"). // ユーザー名を設定
        Save(ctx)     // エンティティをデータベースに保存するためにSaveを呼び出します
    if err != nil {
        return nil, fmt.Errorf("ユーザーの作成に失敗しました: %w", err)
    }
    log.Println("ユーザーが作成されました: ", u)
    return u, nil
}

main関数では、CreateUser関数を呼び出して新しいユーザーエンティティを作成できます。

func main() {
    // ...データベース接続確立コードは省略

    // ユーザーエンティティを作成
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("ユーザーの作成に失敗しました: %v", err)
    }
    log.Printf("作成されたユーザー: %#v\n", u)
}

4.3. エンティティのクエリ

エンティティをクエリするために、entによって生成されたクエリビルダーを使用することができます。以下のコードは、"a8m"という名前のユーザーをクエリする方法を示しています。

// QueryUser関数は指定された名前のユーザーエンティティをクエリするために使用されます
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // client.User.Query()を使用してUserのクエリを構築し、
    // その後、Whereメソッドをチェーンしてusernameでのクエリなど、クエリ条件を追加します
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // クエリ条件を追加します。この場合、名前は "a8m" です
        Only(ctx)                      // Onlyメソッドは1つの結果のみが期待されていることを示します
    if err != nil {
        return nil, fmt.Errorf("ユーザーのクエリに失敗しました: %w", err)
    }
    log.Println("ユーザーが返されました: ", u)
    return u, nil
}

main関数では、QueryUser関数を呼び出してユーザーエンティティをクエリできます。

func main() {
    // ...データベース接続確立とユーザー作成コードは省略

    // ユーザーエンティティをクエリ
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("ユーザーのクエリに失敗しました: %v", err)
    }
    log.Printf("クエリされたユーザー: %#v\n", u)
}

5.1. エッジと逆エッジの理解

entフレームワークでは、データモデルはグラフ構造として視覚化されます。ここで、エンティティはグラフ内のノードを表し、エンティティ間の関係はエッジで表現されます。エッジは一つのエンティティから別のエンティティへの接続であり、例えば、Userは複数のCarsを所有することができます。

逆エッジはエッジへの逆の参照であり、論理的にはエンティティ間の逆の関係を表しますが、データベースに新しい関係を作成しません。例えば、Carの逆エッジを通じて、この車を所有するUserを見つけることができます。

エッジと逆エッジの主な意義は、関連するエンティティ間のナビゲーションを非常に直感的かつ簡単にすることにあります。

ヒント: entでは、エッジは従来のデータベースの外部キーに対応し、テーブル間の関係を定義するために使用されます。

5.2. スキーマでのエッジの定義

最初に、ent CLIを使用してCarGroupの初期スキーマを作成します。

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

次に、Userスキーマでは、Carとのエッジを定義して、ユーザーと車の関係を表します。ユーザーエンティティ内で、Carタイプを指すcarsというエッジを追加できます。これにより、ユーザーが複数の車を所有できることが示されます。

// entdemo/ent/schema/user.go

// ユーザーのエッジ。
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

エッジを定義した後は、対応するコードを生成するために再度go generate ./entを実行する必要があります。

5.3. エッジデータの操作

ユーザーに関連付けられた車を作成するプロセスは簡単です。ユーザーエンティティが与えられた場合、新しい車エンティティを作成し、ユーザーと関連付けることができます。

import (
    "context"
    "log"
    "entdemo/ent"
    // Carのスキーマ定義をインポートすることを忘れないでください
    _ "entdemo/ent/schema"
)

func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("ユーザーを取得できませんでした: %v", err)
        return err
    }

    // 新しい車を作成し、ユーザーと関連付ける
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("ユーザーのために車を作成できませんでした: %v", err)
        return err
    }

    log.Println("車が作成され、ユーザーと関連付けられました")
    return nil
}

同様に、ユーザーの車をクエリすることも簡単です。ユーザーが所有するすべての車のリストを取得したい場合は、次のように行うことができます。

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("ユーザーを取得できませんでした: %v", err)
        return err
    }

    // ユーザーが所有するすべての車をクエリする
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("車のクエリに失敗しました: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("車: %v, モデル: %v", car.ID, car.Model)
    }
    return nil
}

上記の手順を通じて、スキーマでエッジを定義する方法だけでなく、エッジに関連するデータの作成とクエリ方法も学びました。

6. グラフのトラバースとクエリ

6.1. グラフ構造の理解

entでは、グラフ構造はエンティティとそれらの間のエッジで表されます。各エンティティはグラフ内のノードに相当し、エンティティ間の関係はエッジによって表されます。これにより、リレーショナルデータベース上での複雑なクエリや操作がシンプルで直感的になります。

6.2. グラフ構造のトラバーサル

グラフのトラバーサルコードの記述では、主にエンティティ間のエッジを介してデータのクエリと関連付けが行われます。以下は、entでグラフ構造をトラバースする方法を示す簡単な例です:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal はグラフ構造をトラバースする例です
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // "Ariel"という名前のユーザーをクエリする
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("ユーザーのクエリに失敗しました: %v", err)
        return err
    }

    // Arielが所有するすべての車をトラバースする
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("車のクエリに失敗しました: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Arielはモデルが %s の車を所有しています", car.Model)
    }

    // Arielが所属しているすべてのグループをトラバースする
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("グループのクエリに失敗しました: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Arielはグループ %s のメンバーです", g.Name)
    }

    return nil
}

上記のコードはグラフのトラバースの基本的な例であり、まずユーザーをクエリし、その後、ユーザーの車とグループをトラバースします。

7. データベーススキーマの可視化

7.1. Atlasツールのインストール

entで生成されたデータベーススキーマを可視化するには、Atlasツールを使用できます。Atlasのインストール手順は非常に簡単です。たとえば、macOSでは次のようにしてbrewを使用してインストールできます:

brew install ariga/tap/atlas

注意:Atlasはさまざまなデータベースに対してテーブル構造のバージョン管理を行うことができる汎用のデータベースマイグレーションツールです。Atlasの詳細な紹介は後の章で提供されます。

7.2. ERDおよびSQLスキーマの生成

Atlasを使用してスキーマを表示およびエクスポートするのは非常に簡単です。Atlasをインストールした後、次のコマンドを使用してエンティティ関係図(ERD)を表示できます:

atlas schema inspect -d [database_dsn] --format dot

または、直接SQLスキーマを生成できます:

atlas schema inspect -d [database_dsn] --format sql

ここで、[database_dsn]はデータソース名(DSN)を指します。たとえば、SQLiteデータベースの場合は次のようになります:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

これらのコマンドによって生成された出力は、それぞれのツールを使用してさらにビューまたはドキュメントに変換できます。

8. スキーママイグレーション

8.1. 自動マイグレーションとバージョン管理マイグレーション

entは自動マイグレーションとバージョン管理マイグレーションの2つのスキーママイグレーション戦略をサポートしています。自動マイグレーションはランタイムでスキーマの変更を調査および適用するプロセスであり、開発およびテストに適しています。バージョン管理マイグレーションはマイグレーションスクリプトを生成し、本番展開前に注意深いレビューとテストが必要です。

ヒント:自動マイグレーションについては、セクション4.1の内容を参照してください。

8.2. バージョン管理マイグレーションの実行

バージョン管理マイグレーションのプロセスには、Atlasを使用してマイグレーションファイルを生成する手順が含まれます。以下は関連するコマンドです:

マイグレーションファイルを生成するには:

atlas migrate diff -d ent/schema/path --dir migrations/dir

その後、これらのマイグレーションファイルをデータベースに適用できます:

atlas migrate apply -d migrations/dir --url database_dsn

このプロセスに従うことで、データベースマイグレーションの履歴をバージョン管理システムで維持し、各マイグレーションの前に十分なレビューを確実にすることができます。

ヒント:サンプルコードは次のリンクから参照できます:https://github.com/ent/ent/tree/master/examples/start