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

クイックスタート

ent はGoのためのシンプルかつ強力なエンティティフレームワークです。このフレームワークを使うことで,大量のデータモデルをもつアプリケーションのビルドとメンテナンスを簡単に行うことができ、次のような原則に従うことができます:

  • データベーススキーマをグラフ構造として簡単にモデル化
  • スキーマをプログラムのGoコードとして定義
  • コード生成に基づく静的型付け
  • データベースクエリおよびグラフトラバーサルの記述が容易
  • Goテンプレートを使用することで拡張やカスタマイズが容易

gopher-schema-as-code

インストール#

go get entgo.io/ent/cmd/ent

ent コード生成ツールをインストールした後、ツールへのパスを PATH に追加する必要があります。 If you don't find it your path, you can also run: go run entgo.io/ent/cmd/ent <command>

Go環境のセットアップ#

あなたのプロジェクトディレクトリが GOPATH の外にある、またはGOPATHに慣れていない場合は、下記コマンドにより Go module プロジェクトを設定します。

go mod init <project>

あなたの最初のスキーマを作成する#

あなたのプロジェクトのルートディレクトリに移動し、以下を実行します。

go run entgo.io/ent/cmd/ent init User

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

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

User スキーマに 2 つのフィールドを追加します。

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

プロジェクトのルートディレクトリから以下のようにgo generateを実行します。

go generate ./ent

これにより、以下のファイルが作成されます。

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── migrate
│ ├── migrate.go
│ └── schema.go
├── predicate
│ └── predicate.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

最初のエンティティを作成する#

まず、新しいent.Clientを作成します。 この例では、SQLite3を使用します。

package main
import (
"context"
"log"
"<project>/ent"
_ "github.com/mattn/go-sqlite3"
)
func main() {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
// オートマイグレーションツールを実行する
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

これで、ユーザーを作成する準備が整いました。 例として、この関数をCreateUserと呼びましょう。

func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Create().
SetAge(30).
SetName("a8m").
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", u)
return u, nil
}

エンティティへの問い合わせ#

entは、各エンティティ・スキーマのために、その述語、デフォルト値、バリデーター、ストレージ要素に関する追加情報 (カラム名、主キーなど) を含むパッケージを生成します。

package main
import (
"log"
"<project>/ent"
"<project>/ent/user"
)
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.NameEQ("a8m")).
// ユーザーが見つからない場合、`Only`は失敗する
// あるいは、1人以上のユーザーが返却される
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}

最初のエッジ (リレーション) を追加する#

チュートリアルのこの部分では、スキーマ内の別のエンティティへのエッジ (リレーション) を宣言したいと思います。
CarGroupという名前の2つの追加エンティティをいくつかのフィールドとともに作成してみましょう。 初期スキーマの生成にはent CLIを使用します。

go run entgo.io/ent/cmd/ent init Car Group

そして、残りのフィールドを手動で追加します。

import (
"regexp"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Carのフィールド
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
// Groupのフィールド
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// グループ名の正規表現検証
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}

最初のリレーションを定義しましょう。 UserからCarへのエッジは、ユーザーが1台以上の車を所有できる一方で、車のオーナーは1人のみであることを定義します (一対多の関係)。

er-user-cars

"cars"エッジをUserスキーマに追加して、go generate ./entを実行してみましょう。

import (
"log"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// Userのエッジ
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}

続いて、2台の車を作成し、ユーザーに追加する例を見てみましょう。

func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
// モデル "Tesla"で新しい車を作成する
tesla, err := client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", tesla)
// モデル “Ford”で新しい車を作る
ford, err := client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", ford)
// 新しいユーザーを作成して、2台の車を追加する
a8m, err := client.User.
Create().
SetAge(30).
SetName("a8m").
AddCars(tesla, ford).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", a8m)
return a8m, nil
}

しかし、carsのエッジ (リレーション) を照会する場合はどうでしょうか。 その方法は以下の通りです。

import (
"log"
"<project>/ent"
"<project>/ent/car"
)
func QueryCars(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println("returned cars:", cars)
// 特定の車をフィルタリングするのはどうだろうか
ford, err := a8m.QueryCars().
Where(car.ModelEQ("Ford")).
Only(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println(ford)
return nil
}

最初の逆方向エッジ (BackRef) を追加する#

例えば、Carオブジェクトがあり、この車が属するユーザー、即ちオーナーを取得したいとします。 このために、edge.From関数を使って定義される「逆方向エッジ」という別のタイプのエッジがあります。

er-cars-owner

上の図で作成された新しいエッジは半透明になっており、データベースに別のエッジを作成していないことを強調しています。 本当のエッジ (リレーション) の後方参照に過ぎません。

Carスキーマにownerという名前の逆方向エッジを追加し、それがUserスキーマのcarsエッジへの参照であることを示し、go generate ./entを実行してみましょう。

import (
"log"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// Carのエッジ
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// `User` タイプの "owner" という逆方向エッジを作成し、
// `Ref` メソッドを用いて明示的に (User スキーマの)
// "cars" エッジを参照させる
edge.From("owner", User.Type).
Ref("cars").
// エッジをユニークに設定することで、車の
// オーナーは一人のみであることを保証する
Unique(),
}
}

上記のuser/carsの例の続きとして、逆方向エッジを照会してみます。

import (
"fmt"
"log"
"<project>/ent"
)
func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
// 逆方向エッジを問い合わせる
for _, ca := range cars {
owner, err := ca.QueryOwner().Only(ctx)
if err != nil {
return fmt.Errorf("failed querying car %q owner: %w", ca.Model, err)
}
log.Printf("car %q owner: %q\n", ca.Model, owner.Name)
}
return nil
}

2つ目のエッジを作成する#

今回の例では、ユーザーとグループの間にM2M (多対多) のリレーションを作ります。

er-group-users

ご覧のように、各グループエンティティは多くのユーザーを持つことができ、また、ユーザーは多くのグループに接続することができます。単純な「多対多」の関係です。 上の図では、Groupスキーマがusersというエッジ (リレーション) の所有者であり、Userエンティティはgroupsという名前の、このリレーションへの後方参照/逆方向エッジを持っています。 この関係をスキーマで定義してみましょう。

  • <project>/ent/schema/group.go:

    import (
    "log"
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    )
    // Groupのエッジ
    func (Group) Edges() []ent.Edge {
    return []ent.Edge{
    edge.To("users", User.Type),
    }
    }
  • <project>/ent/schema/user.go:

    import (
    "log"
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    )
    // Userのエッジ
    func (User) Edges() []ent.Edge {
    return []ent.Edge{
    edge.To("cars", Car.Type),
    // `Group`タイプの"groups"という逆エッジを作成し、
    // `Ref`メソッドを使って明示的に (Groupスキーマの)
    // “users”エッジを参照する
    edge.From("groups", Group.Type).
    Ref("users"),
    }
    }

スキーマディレクトリでentを実行し、アセットを再生成します。

go generate ./ent

はじめてのグラフ探索#

最初のグラフ探索を実行するためには、いくつかのデータ (ノードとエッジ、言い換えれば、エンティティとリレーション) を生成する必要があります。 フレームワークを使って、次のようなグラフを作成してみましょう。

re-graph

func CreateGraph(ctx context.Context, client *ent.Client) error {
// 最初に、ユーザーを複数作成する
a8m, err := client.User.
Create().
SetAge(30).
SetName("Ariel").
Save(ctx)
if err != nil {
return err
}
neta, err := client.User.
Create().
SetAge(28).
SetName("Neta").
Save(ctx)
if err != nil {
return err
}
// その後、車を複数作成し、作成したユーザーにアタッチする
_, err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()). // グラフ内の時間を無視する
SetOwner(a8m). // このグラフをユーザー"Ariel"にアタッチする
Save(ctx)
if err != nil {
return err
}
_, err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()). // グラフ内の時間を無視する
SetOwner(a8m). // このグラフをユーザー"Ariel"にアタッチする
Save(ctx)
if err != nil {
return err
}
_, err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()). // グラフ内の時間を無視する
SetOwner(neta). // このグラフをユーザー"Neta"にアタッチする
Save(ctx)
if err != nil {
return err
}
// グループを作成し、作成中のユーザを追加する
_, err = client.Group.
Create().
SetName("GitLab").
AddUsers(neta, a8m).
Save(ctx)
if err != nil {
return err
}
_, err = client.Group.
Create().
SetName("GitHub").
AddUsers(a8m).
Save(ctx)
if err != nil {
return err
}
log.Println("The graph was created successfully")
return nil
}

データを含むグラフを作成したら、いくつかのクエリを実行できます。

  1. "GitHub "というグループ内のすべてのユーザーの車を取得します。

    import (
    "log"
    "<project>/ent"
    "<project>/ent/group"
    )
    func QueryGithub(ctx context.Context, client *ent.Client) error {
    cars, err := client.Group.
    Query().
    Where(group.Name("GitHub")). // (Group(Name=GitHub),)
    QueryUsers(). // (User(Name=Ariel, Age=30),)
    QueryCars(). // (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    All(ctx)
    if err != nil {
    return fmt.Errorf("failed getting cars: %w", err)
    }
    log.Println("cars returned:", cars)
    // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    return nil
    }
  2. 上のクエリを変更して、トラバーサルのソースがユーザー Arielになるようにします。

    import (
    "log"
    "<project>/ent"
    "<project>/ent/car"
    )
    func QueryArielCars(ctx context.Context, client *ent.Client) error {
    // 前の手順でユーザー "Ariel" を入手する
    a8m := client.User.
    Query().
    Where(
    user.HasCars(),
    user.Name("Ariel"),
    ).
    OnlyX(ctx)
    cars, err := a8m. // a8mが接続されているグループを取得する
    QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
    QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
    QueryCars(). //
    Where( //
    car.Not( // ユーザー"Neta"と"Ariel"の車を取得するが、フィルタリングを行う
    car.ModelEQ("Mazda"), // それらの車のモデルは"Mazda"である
    ), //
    ). //
    All(ctx)
    if err != nil {
    return fmt.Errorf("failed getting cars: %w", err)
    }
    log.Println("cars returned:", cars)
    // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
    return nil
    }
  3. ユーザーを持つすべてのグループを取得します (索引述語付きのクエリ)。

    import (
    "log"
    "<project>/ent"
    "<project>/ent/group"
    )
    func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
    groups, err := client.Group.
    Query().
    Where(group.HasUsers()).
    All(ctx)
    if err != nil {
    return fmt.Errorf("failed getting groups: %w", err)
    }
    log.Println("groups returned:", groups)
    // Output: (Group(Name=GitHub), Group(Name=GitLab),)
    return nil
    }

完全なサンプルは、GitHubにあります。