Ent
Ent is a Go entity framework that adopts a "Schema as Code" approach. Inspired by an entity framework used internally at Meta (Facebook), it provides complete type safety and explicit APIs through code generation. Supporting multiple databases including MySQL, PostgreSQL, SQLite, and CockroachDB, it offers comprehensive enterprise-level features such as GraphQL integration, advanced migration with Atlas integration, and graph traversal queries as a modern Go ORM solution.
GitHub Overview
Topics
Star History
Library
Ent
Overview
Ent is a Go entity framework that adopts a "Schema as Code" approach. Inspired by an entity framework used internally at Meta (Facebook), it provides complete type safety and explicit APIs through code generation. Supporting multiple databases including MySQL, PostgreSQL, SQLite, and CockroachDB, it offers comprehensive enterprise-level features such as GraphQL integration, advanced migration with Atlas integration, and graph traversal queries as a modern Go ORM solution.
Details
Ent 2025 edition continues to evolve as a mature Go entity framework designed based on practical insights from Meta (Facebook). By defining schemas in Go code and automatically generating 100% type-safe APIs, it achieves both compile-time error detection and runtime safety. It enables intuitive modeling of relational data and efficient complex data retrieval through graph structure query traversal. With genuine schema migration through Atlas integration, automatic GraphQL generation, and rich extension features, it provides a comprehensive data layer solution for modern Go development.
Key Features
- Schema as Code: Intuitive schema definition in Go code with type safety
- Complete Code Generation: 100% type-safe auto-generated APIs reducing runtime errors
- Graph Traversal Queries: Efficient traversal and retrieval of complex relational data
- Multi-Database Support: Unified API for MySQL, PostgreSQL, SQLite, CockroachDB, etc.
- Atlas Integration: Professional schema migration and version management
- Automatic GraphQL Generation: Automated GraphQL server construction from schemas
Pros and Cons
Pros
- High reliability and practicality based on proven design philosophy from Meta (Facebook)
- Complete type safety enabling compile-time error detection and runtime safety
- Code centralization and version control through Schema as Code approach
- Intuitive and readable query writing with fluent API
- Significant efficiency improvement in API development through automatic GraphQL generation
- Enterprise-level migration management through Atlas integration
Cons
- Build process complexity due to dependency on Go code generation
- Project size increase due to auto-generated code
- Cannot be used in non-Go language projects due to Go-specific design
- Learning cost and need to master Ent-specific concepts
- Constraints compared to raw SQL or other ORMs for complex queries
- Smaller track record and community compared to Gorm due to being relatively new library
Reference Pages
Code Examples
Basic Setup
# Install Ent
go mod init example.com/my-project
go get entgo.io/ent/cmd/ent
# Initial schema generation
go run entgo.io/ent/cmd/ent@latest new User Pet
# Code generation
go generate ./ent
# Add necessary dependencies
go get entgo.io/ent/dialect/sql/sqlite3
go get modernc.org/sqlite
// main.go - Basic setup
package main
import (
"context"
"fmt"
"log"
"example.com/my-project/ent"
"entgo.io/ent/dialect"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// Create client
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()
// Run schema migration
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
fmt.Println("Database schema created successfully!")
}
Model Definition and Basic Operations
// ent/schema/user.go - User schema definition
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("User name"),
field.String("email").
Unique().
Comment("Email address"),
field.Int("age").
Positive().
Comment("Age"),
field.Time("created_at").
Default(time.Now).
Comment("Creation time"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
Comment("Owned pets"),
edge.To("posts", Post.Type).
Comment("Published posts"),
}
}
// ent/schema/pet.go - Pet schema definition
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("Pet type"),
field.Int("age").
NonNegative().
Comment("Pet age"),
}
}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique().
Comment("Owner"),
}
}
// Basic CRUD operations
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()
// Schema migration
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Create user (Create)
user, err := CreateUser(ctx, client)
if err != nil {
log.Fatal(err)
}
fmt.Printf("created user: %+v\n", user)
// Query user (Read)
user, err = QueryUser(ctx, client, user.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("queried user: %+v\n", user)
// Update user (Update)
user, err = UpdateUser(ctx, client, user.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("updated user: %+v\n", user)
// Delete 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("John Doe").
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
}
Advanced Query Operations
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"
)
// Advanced query examples
func AdvancedQueries(ctx context.Context, client *ent.Client) {
// Multi-condition search
users, err := client.User.
Query().
Where(
user.And(
user.AgeGT(20),
user.AgeLT(50),
user.NameContains("John"),
),
).
Order(ent.Asc(user.FieldCreatedAt)).
Limit(10).
All(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("filtered users: %v\n", users)
// Eager loading with relationships
users, err = client.User.
Query().
WithPets(). // Load pet information
WithPosts(). // Load post information
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))
}
// Aggregation queries
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])
// Graph traversal queries
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)
// Subquery usage
usersWithPets, err := client.User.
Query().
Where(user.HasPets()).
All(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Users with pets: %v\n", usersWithPets)
// Custom condition search
recentUsers, err := client.User.
Query().
Where(
user.CreatedAtGT(time.Now().AddDate(0, -1, 0)), // Within 1 month
).
Order(ent.Desc(user.FieldCreatedAt)).
All(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recent users: %v\n", recentUsers)
}
// Dynamic query building
func DynamicQuery(ctx context.Context, client *ent.Client,
name string, minAge, maxAge int, hasPets bool) ([]*ent.User, error) {
query := client.User.Query()
// Add conditions dynamically
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)
}
Relationship Operations
// ent/schema/post.go - Post schema (many-to-one relationship)
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(), // Foreign key field
}
}
func (Post) Edges() []ent.Edge {
return []ent.Edge{
edge.From("author", User.Type).
Ref("posts").
Field("author_id"). // Bind to foreign key field
Unique(),
edge.To("tags", Tag.Type), // Many-to-many relationship
}
}
// ent/schema/tag.go - Tag schema (many-to-many relationship)
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"),
}
}
// Relationship operation examples
package main
import (
"context"
"fmt"
"log"
"time"
"example.com/my-project/ent"
)
func RelationshipOperations(ctx context.Context, client *ent.Client) {
// Create user and pets (one-to-many)
user, err := client.User.
Create().
SetName("Jane Smith").
SetEmail("[email protected]").
SetAge(28).
Save(ctx)
if err != nil {
log.Fatal(err)
}
pet1, err := client.Pet.
Create().
SetName("Buddy").
SetType(pet.TypeDog).
SetAge(3).
SetOwner(user). // Set relationship
Save(ctx)
if err != nil {
log.Fatal(err)
}
pet2, err := client.Pet.
Create().
SetName("Whiskers").
SetType(pet.TypeCat).
SetAge(2).
SetOwner(user).
Save(ctx)
if err != nil {
log.Fatal(err)
}
// Create posts and tags (many-to-many)
tag1, err := client.Tag.
Create().
SetName("Technology").
SetDescription("Technology-related posts").
Save(ctx)
if err != nil {
log.Fatal(err)
}
tag2, err := client.Tag.
Create().
SetName("Go Language").
SetDescription("Go language-related posts").
Save(ctx)
if err != nil {
log.Fatal(err)
}
post, err := client.Post.
Create().
SetTitle("How to Use Ent").
SetContent("Ent is an amazing ORM...").
SetAuthor(user).
AddTags(tag1, tag2). // Associate multiple tags
SetPublishedAt(time.Now()).
Save(ctx)
if err != nil {
log.Fatal(err)
}
// Query relationship data
// User's pets
userPets, err := user.QueryPets().All(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User %s has %d pets\n", user.Name, len(userPets))
// Post's tags
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()
// Pet's owner
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)
// Complex relationship searches
// Authors of posts with "Technology" tag
techAuthors, err := client.User.
Query().
Where(user.HasPostsWith(
post.HasTagsWith(tag.Name("Technology")),
)).
All(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Authors with tech posts: %v\n", techAuthors)
// Update relationships
// Transfer pet to another user
newOwner, err := client.User.
Create().
SetName("Bob Johnson").
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)
}
// Remove relationships
// Remove tag from post
post, err = client.Post.
UpdateOne(post).
RemoveTags(tag2).
Save(ctx)
if err != nil {
log.Fatal(err)
}
}
Practical Examples
// Service layer implementation example
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}
}
// User registration
func (s *UserService) CreateUser(ctx context.Context, name, email string, age int) (*ent.User, error) {
// Check for duplicates
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)
}
// Create user
return s.client.User.
Create().
SetName(name).
SetEmail(email).
SetAge(age).
Save(ctx)
}
// Get user details (including posts)
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)
}
// Search users
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)
}
// User statistics
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
}
// Transaction usage example
func (s *UserService) TransferPet(ctx context.Context, petID, fromUserID, toUserID int) error {
// Start transaction
tx, err := s.client.Tx(ctx)
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
// Change pet ownership
_, err = tx.Pet.
UpdateOneID(petID).
SetOwnerID(toUserID).
Save(ctx)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to update pet owner: %w", err)
}
// Record history (example)
_, 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)
}
// Commit
return tx.Commit()
}
// HTTP handler integration example (using 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()
// User list
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)
})
// User details
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)
})
// Create 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() {
// Database connection
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()
// Run migration
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Initialize service
userService := service.NewUserService(client)
// Start server
r := setupRoutes(userService)
r.Run(":8080")
}