Ent

Entは「Schema as Code(コードとしてのスキーマ)アプローチ」を採用したGo言語向けエンティティフレームワークです。Meta(Facebook)で内部的に使用されていたエンティティフレームワークからインスパイアされ、完全な型安全性とコード生成による明示的なAPIを提供。MySQL、PostgreSQL、SQLite、CockroachDBなど複数データベースをサポートし、GraphQL統合、Atlas連携による高度なマイグレーション、グラフ横断クエリなど企業レベルの機能を包括的にサポートする現代的なGo ORMソリューションです。

ORMGoスキーマ生成GraphQLマイグレーション型安全

GitHub概要

ent/ent

An entity framework for Go

ホームページ:https://entgo.io
スター16,368
ウォッチ152
フォーク970
作成日:2019年6月12日
言語:Go
ライセンス:Apache License 2.0

トピックス

ententity-frameworkorm

スター履歴

ent/ent Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

Ent

概要

Entは「Schema as Code(コードとしてのスキーマ)アプローチ」を採用したGo言語向けエンティティフレームワークです。Meta(Facebook)で内部的に使用されていたエンティティフレームワークからインスパイアされ、完全な型安全性とコード生成による明示的なAPIを提供。MySQL、PostgreSQL、SQLite、CockroachDBなど複数データベースをサポートし、GraphQL統合、Atlas連携による高度なマイグレーション、グラフ横断クエリなど企業レベルの機能を包括的にサポートする現代的なGo ORMソリューションです。

詳細

Ent 2025年版は、Meta(Facebook)の実践的な知見を基に設計された成熟したGo言語エンティティフレームワークとして進化し続けています。スキーマ定義をGoコードで記述し、100%型安全なAPIを自動生成することで、コンパイル時エラー検出と実行時安全性を両立。関係性のあるデータを直感的にモデリングし、グラフ構造のクエリ横断により複雑なデータ取得を効率的に実現。Atlasとの統合による本格的なスキーママイグレーション、GraphQL自動生成、豊富な拡張機能により、モダンなGo開発における包括的なデータ層ソリューションを提供します。

主な特徴

  • Schema as Code: Goコードによる直感的なスキーマ定義と型安全性
  • 完全コード生成: 100%型安全な自動生成APIによる実行時エラー削減
  • グラフ横断クエリ: 複雑な関係性データの効率的な横断と取得
  • マルチDB対応: MySQL、PostgreSQL、SQLite、CockroachDB等への統一API
  • Atlas統合: 本格的なスキーママイグレーションとバージョン管理
  • GraphQL自動生成: スキーマからGraphQLサーバーの自動構築

メリット・デメリット

メリット

  • Meta(Facebook)の実績ある設計思想による高い信頼性と実用性
  • 完全な型安全性によるコンパイル時エラー検出と実行時安全性
  • Schema as Codeアプローチによるバージョン管理とコード一元化
  • 流暢なAPIによる直感的で読みやすいクエリ記述
  • GraphQL自動生成によるAPI開発の大幅な効率化
  • Atlasとの統合による企業レベルのマイグレーション管理

デメリット

  • Goコード生成への依存によるビルドプロセスの複雑化
  • 自動生成されるコードによるプロジェクトサイズの増大
  • Go言語特化設計による他言語プロジェクトでの利用不可
  • 学習コストとEnt特有の概念習得が必要
  • 複雑なクエリでの生SQLや他ORMとの比較での制約
  • 比較的新しいライブラリのため実績とコミュニティがGorm等より小規模

参考ページ

書き方の例

基本セットアップ

# Entのインストール
go mod init example.com/my-project
go get entgo.io/ent/cmd/ent

# スキーマの初期生成
go run entgo.io/ent/cmd/ent@latest new User Pet

# コード生成
go generate ./ent

# 必要な依存関係の追加
go get entgo.io/ent/dialect/sql/sqlite3
go get modernc.org/sqlite
// main.go - 基本的なセットアップ
package main

import (
    "context"
    "fmt"
    "log"

    "example.com/my-project/ent"
    "entgo.io/ent/dialect"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // クライアント作成
    client, err := ent.Open(dialect.SQLite, "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)
    }

    fmt.Println("Database schema created successfully!")
}

モデル定義と基本操作

// ent/schema/user.go - ユーザースキーマ定義
package schema

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

// User holds the schema definition for the User entity.
type User struct {
    ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            NotEmpty().
            Comment("ユーザー名"),
        field.String("email").
            Unique().
            Comment("メールアドレス"),
        field.Int("age").
            Positive().
            Comment("年齢"),
        field.Time("created_at").
            Default(time.Now).
            Comment("作成日時"),
    }
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type).
            Comment("飼っているペット"),
        edge.To("posts", Post.Type).
            Comment("投稿した記事"),
    }
}

// ent/schema/pet.go - ペットスキーマ定義
package schema

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

type Pet struct {
    ent.Schema
}

func (Pet) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            NotEmpty(),
        field.Enum("type").
            Values("dog", "cat", "bird", "fish").
            Comment("ペットの種類"),
        field.Int("age").
            NonNegative().
            Comment("ペットの年齢"),
    }
}

func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type).
            Ref("pets").
            Unique().
            Comment("飼い主"),
    }
}

// 基本的なCRUD操作
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "example.com/my-project/ent"
    "example.com/my-project/ent/user"
    "example.com/my-project/ent/pet"
)

func main() {
    ctx := context.Background()
    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(ctx); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }

    // ユーザー作成(Create)
    user, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("created user: %+v\n", user)

    // ユーザー取得(Read)
    user, err = QueryUser(ctx, client, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("queried user: %+v\n", user)

    // ユーザー更新(Update)
    user, err = UpdateUser(ctx, client, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("updated user: %+v\n", user)

    // ユーザー削除(Delete)
    err = DeleteUser(ctx, client, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("user deleted successfully")
}

func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    u, err := client.User.
        Create().
        SetName("田中太郎").
        SetEmail("[email protected]").
        SetAge(30).
        SetCreatedAt(time.Now()).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed creating user: %w", err)
    }
    return u, nil
}

func QueryUser(ctx context.Context, client *ent.Client, id int) (*ent.User, error) {
    u, err := client.User.
        Query().
        Where(user.ID(id)).
        Only(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed querying user: %w", err)
    }
    return u, nil
}

func UpdateUser(ctx context.Context, client *ent.Client, id int) (*ent.User, error) {
    u, err := client.User.
        UpdateOneID(id).
        SetAge(31).
        SetEmail("[email protected]").
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed updating user: %w", err)
    }
    return u, nil
}

func DeleteUser(ctx context.Context, client *ent.Client, id int) error {
    err := client.User.
        DeleteOneID(id).
        Exec(ctx)
    if err != nil {
        return fmt.Errorf("failed deleting user: %w", err)
    }
    return nil
}

高度なクエリ操作

package main

import (
    "context"
    "fmt"
    "log"

    "example.com/my-project/ent"
    "example.com/my-project/ent/user"
    "example.com/my-project/ent/pet"
    "example.com/my-project/ent/post"
)

// 高度なクエリの例
func AdvancedQueries(ctx context.Context, client *ent.Client) {
    // 複数条件での検索
    users, err := client.User.
        Query().
        Where(
            user.And(
                user.AgeGT(20),
                user.AgeLT(50),
                user.NameContains("田中"),
            ),
        ).
        Order(ent.Asc(user.FieldCreatedAt)).
        Limit(10).
        All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("filtered users: %v\n", users)

    // 関係性を含む取得(Eager Loading)
    users, err = client.User.
        Query().
        WithPets().  // ペット情報も一緒に取得
        WithPosts(). // 投稿情報も一緒に取得
        All(ctx)
    if err != nil {
        log.Fatal(err)
    }

    for _, u := range users {
        fmt.Printf("User: %s, Pets: %d, Posts: %d\n", 
            u.Name, len(u.Edges.Pets), len(u.Edges.Posts))
    }

    // 集約クエリ
    ageStats, err := client.User.
        Query().
        Aggregate(
            ent.Mean(user.FieldAge),
            ent.Max(user.FieldAge),
            ent.Min(user.FieldAge),
            ent.Count(),
        ).
        Float64s(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Age stats - Mean: %.2f, Max: %.0f, Min: %.0f, Count: %.0f\n", 
        ageStats[0], ageStats[1], ageStats[2], ageStats[3])

    // グラフ横断クエリ
    petOwners, err := client.Pet.
        Query().
        Where(pet.TypeEQ(pet.TypeDog)).
        QueryOwner().
        Where(user.AgeGT(25)).
        All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Dog owners over 25: %v\n", petOwners)

    // サブクエリ使用
    usersWithPets, err := client.User.
        Query().
        Where(user.HasPets()).
        All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Users with pets: %v\n", usersWithPets)

    // カスタム条件での検索
    recentUsers, err := client.User.
        Query().
        Where(
            user.CreatedAtGT(time.Now().AddDate(0, -1, 0)), // 1ヶ月以内
        ).
        Order(ent.Desc(user.FieldCreatedAt)).
        All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Recent users: %v\n", recentUsers)
}

// 動的クエリ構築
func DynamicQuery(ctx context.Context, client *ent.Client, 
                  name string, minAge, maxAge int, hasPets bool) ([]*ent.User, error) {
    query := client.User.Query()

    // 条件を動的に追加
    var predicates []predicate.User

    if name != "" {
        predicates = append(predicates, user.NameContains(name))
    }

    if minAge > 0 {
        predicates = append(predicates, user.AgeGTE(minAge))
    }

    if maxAge > 0 {
        predicates = append(predicates, user.AgeLTE(maxAge))
    }

    if hasPets {
        predicates = append(predicates, user.HasPets())
    }

    if len(predicates) > 0 {
        query = query.Where(user.And(predicates...))
    }

    return query.All(ctx)
}

リレーション操作

// ent/schema/post.go - 投稿スキーマ(多対一の関係)
package schema

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

type Post struct {
    ent.Schema
}

func (Post) Fields() []ent.Field {
    return []ent.Field{
        field.String("title").
            NotEmpty(),
        field.Text("content"),
        field.Time("published_at").
            Optional(),
        field.Int("author_id").
            Optional(),  // 外部キーフィールド
    }
}

func (Post) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("author", User.Type).
            Ref("posts").
            Field("author_id").  // 外部キーフィールドとの紐付け
            Unique(),
        edge.To("tags", Tag.Type),  // 多対多の関係
    }
}

// ent/schema/tag.go - タグスキーマ(多対多の関係)
package schema

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

type Tag struct {
    ent.Schema
}

func (Tag) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            Unique().
            NotEmpty(),
        field.String("description").
            Optional(),
    }
}

func (Tag) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("posts", Post.Type).
            Ref("tags"),
    }
}

// リレーション操作の例
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "example.com/my-project/ent"
)

func RelationshipOperations(ctx context.Context, client *ent.Client) {
    // ユーザーとペットの作成(一対多)
    user, err := client.User.
        Create().
        SetName("山田花子").
        SetEmail("[email protected]").
        SetAge(28).
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    pet1, err := client.Pet.
        Create().
        SetName("ポチ").
        SetType(pet.TypeDog).
        SetAge(3).
        SetOwner(user).  // リレーション設定
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    pet2, err := client.Pet.
        Create().
        SetName("タマ").
        SetType(pet.TypeCat).
        SetAge(2).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 投稿とタグの作成(多対多)
    tag1, err := client.Tag.
        Create().
        SetName("技術").
        SetDescription("技術関連の投稿").
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    tag2, err := client.Tag.
        Create().
        SetName("Go言語").
        SetDescription("Go言語関連の投稿").
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    post, err := client.Post.
        Create().
        SetTitle("Entの使い方").
        SetContent("Entは素晴らしいORMです...").
        SetAuthor(user).
        AddTags(tag1, tag2).  // 複数のタグを関連付け
        SetPublishedAt(time.Now()).
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // リレーションデータの取得
    // ユーザーのペット一覧
    userPets, err := user.QueryPets().All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("User %s has %d pets\n", user.Name, len(userPets))

    // 投稿のタグ一覧
    postTags, err := post.QueryTags().All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Post '%s' has tags: ", post.Title)
    for _, tag := range postTags {
        fmt.Printf("%s ", tag.Name)
    }
    fmt.Println()

    // ペットの飼い主
    petOwner, err := pet1.QueryOwner().Only(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Pet %s is owned by %s\n", pet1.Name, petOwner.Name)

    // 複雑なリレーション検索
    // "技術"タグが付いた投稿の作者一覧
    techAuthors, err := client.User.
        Query().
        Where(user.HasPostsWith(
            post.HasTagsWith(tag.Name("技術")),
        )).
        All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Authors with tech posts: %v\n", techAuthors)

    // リレーションの更新
    // ペットを別のユーザーに移譲
    newOwner, err := client.User.
        Create().
        SetName("佐藤次郎").
        SetEmail("[email protected]").
        SetAge(35).
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    pet1, err = client.Pet.
        UpdateOne(pet1).
        SetOwner(newOwner).
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // リレーションの削除
    // 投稿からタグを削除
    post, err = client.Post.
        UpdateOne(post).
        RemoveTags(tag2).
        Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
}

実用例

// サービス層の実装例
package service

import (
    "context"
    "fmt"

    "example.com/my-project/ent"
    "example.com/my-project/ent/user"
    "example.com/my-project/ent/post"
)

type UserService struct {
    client *ent.Client
}

func NewUserService(client *ent.Client) *UserService {
    return &UserService{client: client}
}

// ユーザー登録
func (s *UserService) CreateUser(ctx context.Context, name, email string, age int) (*ent.User, error) {
    // 重複チェック
    exists, err := s.client.User.
        Query().
        Where(user.Email(email)).
        Exist(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to check user existence: %w", err)
    }
    if exists {
        return nil, fmt.Errorf("user with email %s already exists", email)
    }

    // ユーザー作成
    return s.client.User.
        Create().
        SetName(name).
        SetEmail(email).
        SetAge(age).
        Save(ctx)
}

// ユーザー詳細取得(投稿も含む)
func (s *UserService) GetUserWithPosts(ctx context.Context, userID int) (*ent.User, error) {
    return s.client.User.
        Query().
        Where(user.ID(userID)).
        WithPosts(func(q *ent.PostQuery) {
            q.Order(ent.Desc(post.FieldPublishedAt)).
              Limit(10)
        }).
        Only(ctx)
}

// ユーザー検索
func (s *UserService) SearchUsers(ctx context.Context, query string, limit int) ([]*ent.User, error) {
    return s.client.User.
        Query().
        Where(
            user.Or(
                user.NameContains(query),
                user.EmailContains(query),
            ),
        ).
        Limit(limit).
        All(ctx)
}

// ユーザー統計
func (s *UserService) GetUserStats(ctx context.Context) (map[string]interface{}, error) {
    totalUsers, err := s.client.User.Query().Count(ctx)
    if err != nil {
        return nil, err
    }

    avgAge, err := s.client.User.
        Query().
        Aggregate(ent.Mean(user.FieldAge)).
        Float64(ctx)
    if err != nil {
        return nil, err
    }

    activeUsers, err := s.client.User.
        Query().
        Where(user.HasPosts()).
        Count(ctx)
    if err != nil {
        return nil, err
    }

    return map[string]interface{}{
        "total_users":  totalUsers,
        "average_age":  avgAge,
        "active_users": activeUsers,
    }, nil
}

// トランザクション使用例
func (s *UserService) TransferPet(ctx context.Context, petID, fromUserID, toUserID int) error {
    // トランザクション開始
    tx, err := s.client.Tx(ctx)
    if err != nil {
        return fmt.Errorf("failed to start transaction: %w", err)
    }

    // ペットの所有者変更
    _, err = tx.Pet.
        UpdateOneID(petID).
        SetOwnerID(toUserID).
        Save(ctx)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("failed to update pet owner: %w", err)
    }

    // 履歴記録(例)
    _, err = tx.TransferHistory.
        Create().
        SetPetID(petID).
        SetFromUserID(fromUserID).
        SetToUserID(toUserID).
        SetTransferredAt(time.Now()).
        Save(ctx)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("failed to create transfer history: %w", err)
    }

    // コミット
    return tx.Commit()
}

// HTTPハンドラーとの統合例(Gin使用)
package main

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "example.com/my-project/ent"
    "example.com/my-project/service"
)

func setupRoutes(userService *service.UserService) *gin.Engine {
    r := gin.Default()

    // ユーザー一覧
    r.GET("/users", func(c *gin.Context) {
        query := c.Query("q")
        limit := 10
        
        if l := c.Query("limit"); l != "" {
            if parsed, err := strconv.Atoi(l); err == nil {
                limit = parsed
            }
        }

        users, err := userService.SearchUsers(c.Request.Context(), query, limit)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, users)
    })

    // ユーザー詳細
    r.GET("/users/:id", func(c *gin.Context) {
        id, err := strconv.Atoi(c.Param("id"))
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
            return
        }

        user, err := userService.GetUserWithPosts(c.Request.Context(), id)
        if ent.IsNotFound(err) {
            c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
            return
        }
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, user)
    })

    // ユーザー作成
    r.POST("/users", func(c *gin.Context) {
        var req struct {
            Name  string `json:"name" binding:"required"`
            Email string `json:"email" binding:"required,email"`
            Age   int    `json:"age" binding:"required,min=1"`
        }

        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        user, err := userService.CreateUser(c.Request.Context(), req.Name, req.Email, req.Age)
        if err != nil {
            c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusCreated, user)
    })

    return r
}

func main() {
    // データベース接続
    client, err := ent.Open("sqlite3", "file:ent.db?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)
    }

    // サービス初期化
    userService := service.NewUserService(client)

    // サーバー起動
    r := setupRoutes(userService)
    r.Run(":8080")
}