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