Skip to Content
Go Realm v1 is released 🎉
ENT GuideENT PlaygroundLaravel Eloquent to Ent

Laravel Eloquent āĻĨ⧇āϕ⧇ Ent Framework āĻ Migration

📋 āϏ⧂āϚāĻŋāĻĒāĻ¤ā§āϰ

  1. Laravel vs Ent - āĻĻā§āϰ⧁āϤ āϤ⧁āϞāύāĻž
  2. Mixins - āϕ⧋āĻĄ āĻĒ⧁āύāĻ°ā§āĻŦā§āϝāĻŦāĻšāĻžāϰ
  3. Hooks - Event Handling
  4. Interceptors - Query Modification
  5. Privacy Policy - Global Scopes
  6. Step-by-Step Professional Implementation

🔄 Laravel vs Ent - āĻĻā§āϰ⧁āϤ āϤ⧁āϞāύāĻž {#laravel-vs-ent-comparison}

Laravel Eloquent āĻāĻŦāĻ‚ Ent Framework āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻŽā§‚āϞ āĻĒāĻžāĻ°ā§āĻĨāĻ•ā§āϝ āĻāĻŦāĻ‚ āϏāĻžāĻĻ⧃āĻļā§āϝ:

Laravel EloquentEnt FrameworkāĻŦā§āϝāĻžāĻ–ā§āϝāĻž
TraitsMixinsāϕ⧋āĻĄ āĻĒ⧁āύāĻ°ā§āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ
Observers/EventsHooksModel lifecycle events handle āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ
Query ScopesInterceptorsQuery modify āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ
Global ScopesPrivacy PolicyāϏāĻŦ query āϤ⧇ automatic filter apply āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ
creating, createdOnCreate hookāύāϤ⧁āύ entry āϤ⧈āϰāĻŋāϰ āϏāĻŽāϝāĻŧ
updating, updatedOnUpdate hookentry update āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ
deleting, deletedOnDelete hookentry delete āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ
SoftDeletes traitMixin with deleted_atSoft delete functionality

🧩 Mixins - āϕ⧋āĻĄ āĻĒ⧁āύāĻ°ā§āĻŦā§āϝāĻŦāĻšāĻžāϰ {#mixins}

Laravel Trait vs Ent Mixin

Laravel āĻ āφāĻĒāύāĻŋ āϝ⧇āĻ­āĻžāĻŦ⧇ āĻ•āϰāϤ⧇āύ:

// Laravel Trait trait Timestamps { public $timestamps = true; } class User extends Model { use Timestamps; }

Ent āĻ āĻāĻ•āχ āĻ•āĻžāϜ:

// Mixin āĻšāϞ⧋ reusable schema components

✅ Mixin āϕ⧀ āĻāĻŦāĻ‚ āϕ⧇āύ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻŦ⧇āύ?

Mixin āĻšāϞ⧋ āĻāĻ•āϟāĻŋ reusable schema component āϝāĻž āĻāĻ•āĻžāϧāĻŋāĻ• schema āϤ⧇ āĻāĻ•āχ fields, edges, hooks, āĻŦāĻž indexes āϝ⧁āĻ•ā§āϤ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤

┌─────────────────────────────────────┐ │ TimeMixin │ │ (created_at, updated_at fields) │ └────────────â”Ŧ────────────────────────┘ │ ┌────────┴────────┐ │ │ ┌───â–ŧ────┐ ┌────â–ŧ────┐ │ User │ │ Post │ └────────┘ └─────────┘

📝 Step-by-Step: TimeMixin āϤ⧈āϰāĻŋ āĻ•āϰāĻž

Step 1: Mixin File āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ

# ent/schema/mixin directory āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ mkdir -p ent/schema/mixin

Step 2: Time Mixin āϞāĻŋāϖ⧁āύ

File: ent/schema/mixin/time_mixin.go

package mixin import ( "time" "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" ) // TimeMixin implements the ent.Mixin for sharing // time fields with package schemas. type TimeMixin struct { mixin.Schema } // Fields of the TimeMixin. func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time("created_at"). Immutable(). // āĻāĻ•āĻŦāĻžāϰ set āĻšāϞ⧇ change āĻ•āϰāĻž āϝāĻžāĻŦ⧇ āύāĻž Default(time.Now), // automatic current time set āĻšāĻŦ⧇ field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now), // āĻĒā§āϰāϤāĻŋāĻŦāĻžāϰ update āĻ āύāϤ⧁āύ time set āĻšāĻŦ⧇ } }

Laravel āĻāϰ āϏāĻžāĻĨ⧇ āϤ⧁āϞāύāĻž:

// Laravel āĻ āĻāϟāĻŋ automatic āĻ›āĻŋāϞ class User extends Model { public $timestamps = true; // created_at, updated_at automatic }

Step 3: Schema āϤ⧇ Mixin āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ

File: ent/schema/user.go

package schema import ( "entgo.io/ent" "entgo.io/ent/schema/field" "your-project/ent/schema/mixin" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Mixin of the User. func (User) Mixin() []ent.Mixin { return []ent.Mixin{ mixin.TimeMixin{}, // TimeMixin āϝ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāϞ⧋ } } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), field.String("email").Unique(), // created_at āĻāĻŦāĻ‚ updated_at automatically āϝ⧁āĻ•ā§āϤ āĻšāϝāĻŧ⧇ āϗ⧇āϛ⧇ } }

đŸ”Ĩ Advanced Mixin: SoftDeleteMixin

Laravel āĻāϰ SoftDeletes trait āĻāϰ āĻŽāϤ⧋:

File: ent/schema/mixin/soft_delete_mixin.go

package mixin import ( "context" "time" "entgo.io/ent" "entgo.io/ent/dialect/sql" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" ) // SoftDeleteMixin implements soft delete functionality type SoftDeleteMixin struct { mixin.Schema } // Fields of the SoftDeleteMixin func (SoftDeleteMixin) Fields() []ent.Field { return []ent.Field{ field.Time("deleted_at"). Optional(). // nullable field Nillable(), // pointer type (*time.Time) } } // Interceptors for soft delete func (SoftDeleteMixin) Interceptors() []ent.Interceptor { return []ent.Interceptor{ // Delete operation āϕ⧇ update āĻ convert āĻ•āϰ⧇ ent.InterceptFunc(func(next ent.Querier) ent.Querier { return ent.QuerierFunc(func(ctx context.Context, q ent.Query) (ent.Value, error) { // āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ deleted_at NULL āĻāĻŽāύ records query āĻ•āϰāĻŦ⧇ if q, ok := q.(*sql.Selector); ok { q.Where(sql.IsNull("deleted_at")) } return next.Query(ctx, q) }) }), } }

Laravel āĻāϰ āϏāĻžāĻĨ⧇ āϤ⧁āϞāύāĻž:

// Laravel class Post extends Model { use SoftDeletes; // deleted_at field āϝ⧁āĻ•ā§āϤ āĻšāϝāĻŧ } // Query āĻ•āϰāϞ⧇ deleted records automatically āĻŦāĻžāĻĻ āϝāĻžāϝāĻŧ $posts = Post::all(); // āĻļ⧁āϧ⧁ non-deleted posts // Deleted posts āĻĻ⧇āĻ–āϤ⧇ āϚāĻžāχāϞ⧇ $posts = Post::withTrashed()->get();

đŸŽŖ Hooks - Event Handling {#hooks}

Laravel Events vs Ent Hooks

Hooks āĻšāϞ⧋ schema lifecycle events āϝāĻž create, update, delete operations āĻāϰ āϏāĻŽāϝāĻŧ execute āĻšāϝāĻŧāĨ¤

┌─────────────────────────────────────────┐ │ Lifecycle Flow │ └─────────────────────────────────────────┘ Create Flow: Input → [OnCreate Hook] → Validation → Database → [OnCreate Return] Update Flow: Input → [OnUpdate Hook] → Validation → Database → [OnUpdate Return] Delete Flow: Query → [OnDelete Hook] → Database → Return

📊 Hook Types āϤ⧁āϞāύāĻž

Laravel EventEnt HookāĻ•āĻ–āύ Execute āĻšāϝāĻŧ
creatingN/A (use default)Before save, āĻ­ā§āϝāĻžāϞāĻŋāĻĄā§‡āĻļāύ⧇āϰ āφāϗ⧇
createdOnCreate returnAfter save successful
updatingN/A (use mutation)Before update
updatedOnUpdate returnAfter update successful
deletingN/ABefore delete
deletedOnDelete returnAfter delete successful
savingMutation builderBefore any save
savedHook returnAfter any save

✅ Hook Types in Ent

Ent āĻ ⧍ āϧāϰāύ⧇āϰ hooks āφāϛ⧇:

  1. Schema Hooks - āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ schema āϰ āϜāĻ¨ā§āϝ
  2. Global Hooks - āϏāĻŦ schemas āĻāϰ āϜāĻ¨ā§āϝ

📝 Step-by-Step: Schema Hooks Implementation

Example 1: Email Normalization Hook (Creating Event āĻāϰ āĻŽāϤ⧋)

File: ent/schema/user.go

package schema import ( "context" "strings" "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" "your-project/ent/hook" ) type User struct { ent.Schema } func (User) Mixin() []ent.Mixin { return []ent.Mixin{ mixin.TimeMixin{}, } } func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), field.String("email").Unique(), field.String("password"), } } // Hooks of the User schema func (User) Hooks() []ent.Hook { return []ent.Hook{ // Hook #1: Email lowercase āĻ•āϰāĻž (creating event) hook.On( func(next ent.Mutator) ent.Mutator { return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) { // Email field āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻž āĻšāĻšā§āϛ⧇ āĻ•āĻŋāύāĻž check āĻ•āϰ⧁āύ if email, ok := m.Email(); ok { // Email āϕ⧇ lowercase āĻ convert āĻ•āϰ⧁āύ m.SetEmail(strings.ToLower(email)) } // āĻĒāϰāĻŦāĻ°ā§āϤ⧀ mutator call āĻ•āϰ⧁āύ return next.Mutate(ctx, m) }) }, // āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ Create āĻāĻŦāĻ‚ Update operation āĻ đŸ”Ĩ ent.OpCreate|ent.OpUpdateOne|ent.OpUpdate, ), // Hook #2: Password hashing (creating/updating event) hook.On( func(next ent.Mutator) ent.Mutator { return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) { // Password field āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻž āĻšāĻšā§āϛ⧇ āĻ•āĻŋāύāĻž if password, ok := m.Password(); ok { // Password hash āĻ•āϰ⧁āύ (bcrypt āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ) hashedPassword, err := bcrypt.GenerateFromPassword( []byte(password), bcrypt.DefaultCost, ) if err != nil { return nil, err } m.SetPassword(string(hashedPassword)) } return next.Mutate(ctx, m) }) }, // āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ Create āĻāĻŦāĻ‚ Update operation āĻ đŸ”Ĩ ent.OpCreate|ent.OpUpdateOne, ), } }

Laravel āĻāϰ āϏāĻžāĻĨ⧇ āϤ⧁āϞāύāĻž:

// Laravel Observer class UserObserver { public function creating(User $user) { // Email lowercase āĻ•āϰāĻž $user->email = strtolower($user->email); // Password hash āĻ•āϰāĻž if ($user->password) { $user->password = bcrypt($user->password); } } public function updating(User $user) { $user->email = strtolower($user->email); if ($user->isDirty('password')) { $user->password = bcrypt($user->password); } } } // App\Providers\EventServiceProvider User::observe(UserObserver::class);

Example 2: Audit Log Hook (Created Event)

File: ent/schema/post.go

package schema import ( "context" "log" "entgo.io/ent" "entgo.io/ent/schema/field" "your-project/ent/hook" ) type Post struct { ent.Schema } func (Post) Fields() []ent.Field { return []ent.Field{ field.String("title"), field.Text("content"), field.Int("user_id"), } } func (Post) Hooks() []ent.Hook { return []ent.Hook{ // Hook: Created event - audit log āϤ⧈āϰāĻŋ āĻ•āϰāĻž hook.On( func(next ent.Mutator) ent.Mutator { return hook.PostFunc(func(ctx context.Context, m *gen.PostMutation) (ent.Value, error) { // Database āĻ save āĻ•āϰ⧁āύ v, err := next.Mutate(ctx, m) if err != nil { return nil, err } // Save successful āĻšāĻ“āϝāĻŧāĻžāϰ āĻĒāϰ audit log āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ post := v.(*gen.Post) log.Printf("New post created: ID=%d, Title=%s, UserID=%d", post.ID, post.Title, post.UserID) // Audit table āĻ entry āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āύ // client.AuditLog.Create(). // SetAction("POST_CREATED"). // SetEntityID(post.ID). // SetUserID(post.UserID). // Save(ctx) return v, nil }) }, ent.OpCreate, // āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ Create operation āĻ ), } }

Laravel āĻāϰ āϏāĻžāĻĨ⧇ āϤ⧁āϞāύāĻž:

class PostObserver { public function created(Post $post) { // Audit log āϤ⧈āϰāĻŋ āĻ•āϰāĻž AuditLog::create([ 'action' => 'POST_CREATED', 'entity_id' => $post->id, 'user_id' => $post->user_id, ]); Log::info("New post created", [ 'id' => $post->id, 'title' => $post->title, ]); } }

🌍 Global Hooks

āϏāĻŦ schemas āĻāϰ āϜāĻ¨ā§āϝ āĻāĻ•āϏāĻžāĻĨ⧇ hooks apply āĻ•āϰāϤ⧇ āϚāĻžāχāϞ⧇:

File: ent/client.go āĻŦāĻž main.go

package main import ( "context" "log" "your-project/ent" "your-project/ent/hook" ) func main() { client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") if err != nil { log.Fatal(err) } defer client.Close() // Global hook āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ client.Use(func(next ent.Mutator) ent.Mutator { return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { // āĻĒā§āϰāϤāĻŋāϟāĻŋ mutation āĻāϰ āφāϗ⧇ log āĻ•āϰ⧁āύ log.Printf("Mutation: %s on %s", m.Op(), m.Type()) // Execute mutation v, err := next.Mutate(ctx, m) // Mutation āĻāϰ āĻĒāϰ if err == nil { log.Printf("Mutation successful: %s", m.Type()) } return v, err }) }) }

🔍 Interceptors - Query Modification {#interceptors}

Laravel Query Scopes vs Ent Interceptors

Interceptors query execution āĻāϰ āφāϗ⧇ āĻŦāĻž āĻĒāϰ⧇ query modify āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤

┌────────────────────────────────────────┐ │ Interceptor Flow │ └────────────────────────────────────────┘ Query Request │ â–ŧ [Interceptor 1] ← Modify query │ â–ŧ [Interceptor 2] ← Add filters │ â–ŧ Database Query │ â–ŧ [Interceptor 3] ← Transform results │ â–ŧ Return Results

📊 Scope Types āϤ⧁āϞāύāĻž

LaravelEntāωāĻĻā§āĻĻ⧇āĻļā§āϝ
Local ScopeQuery HelperReusable query conditions
Global ScopeInterceptorAutomatic query filtering
Dynamic ScopeInterceptor ChainRuntime query modification

✅ Interceptor Types

  1. Traverse Interceptors - Query modify āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ
  2. Query Interceptors - Query execution intercept āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ
  3. Mutation Interceptors - Mutation intercept āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ

📝 Step-by-Step: Interceptor Implementation

Example 1: Published Posts Scope

Laravel Query Scope:

class Post extends Model { // Local scope public function scopePublished($query) { return $query->where('status', 'published'); } // Usage // Post::published()->get(); }

Ent Interceptor (Query Helper):

File: ent/post_query.go (auto-generated āϤ⧇ add āĻ•āϰ⧁āύ)

package ent import ( "context" ) // Published filters published posts only func (pq *PostQuery) Published() *PostQuery { return pq.Where(post.Status("published")) } // Usage: // client.Post.Query().Published().All(ctx)

Example 2: Tenant Isolation Interceptor (Global Scope āĻāϰ āĻŽāϤ⧋)

āĻāϟāĻŋ āĻāĻ•āϟāĻŋ powerful feature - Multi-tenant application āĻāϰ āϜāĻ¨ā§āϝāĨ¤

Laravel Global Scope:

class TenantScope implements Scope { public function apply(Builder $builder, Model $model) { $builder->where('tenant_id', auth()->user()->tenant_id); } } class Post extends Model { protected static function booted() { static::addGlobalScope(new TenantScope); } } // āϏāĻŦ query āϤ⧇ automatic tenant_id filter apply āĻšāĻŦ⧇ $posts = Post::all(); // WHERE tenant_id = ?

Ent Tenant Interceptor:

File: ent/interceptor/tenant.go

package interceptor import ( "context" "fmt" "entgo.io/ent" "entgo.io/ent/dialect/sql" "your-project/ent/post" "your-project/ent/user" ) // TenantKey context key for tenant ID type tenantKey struct{} // WithTenant returns a new context with tenant ID func WithTenant(ctx context.Context, tenantID int) context.Context { return context.WithValue(ctx, tenantKey{}, tenantID) } // GetTenant retrieves tenant ID from context func GetTenant(ctx context.Context) (int, bool) { tenantID, ok := ctx.Value(tenantKey{}).(int) return tenantID, ok } // TenantInterceptor adds tenant_id filter to all queries func TenantInterceptor() ent.Interceptor { return ent.InterceptFunc(func(next ent.Querier) ent.Querier { return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) { // Context āĻĨ⧇āϕ⧇ tenant ID āύāĻŋāύ tenantID, ok := GetTenant(ctx) if !ok { return nil, fmt.Errorf("tenant ID not found in context") } // Query modify āĻ•āϰ⧁āύ switch q := query.(type) { case *PostQuery: // Post query āϤ⧇ tenant_id filter āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ q.Where(post.TenantID(tenantID)) case *UserQuery: // User query āϤ⧇ tenant_id filter āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ q.Where(user.TenantID(tenantID)) // āĻ…āĻ¨ā§āϝāĻžāĻ¨ā§āϝ schema āϰ āϜāĻ¨ā§āϝāĻ“ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ } // Modified query execute āĻ•āϰ⧁āύ return next.Query(ctx, query) }) }) }

Client āĻ Interceptor āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ:

package main import ( "context" "your-project/ent" "your-project/ent/interceptor" ) func main() { client, _ := ent.Open("postgres", "...") // Tenant interceptor āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ client.Intercept(interceptor.TenantInterceptor()) // Usage with tenant context ctx := context.Background() ctx = interceptor.WithTenant(ctx, 123) // Tenant ID = 123 // āĻāχ query automatic āĻ­āĻžāĻŦ⧇ WHERE tenant_id = 123 āϝ⧁āĻ•ā§āϤ āĻ•āϰāĻŦ⧇ posts, _ := client.Post.Query().All(ctx) // āϏāĻŦ posts āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ tenant 123 āĻāϰ āĻšāĻŦ⧇ fmt.Println(posts) }

Example 3: Soft Delete Interceptor

File: ent/interceptor/soft_delete.go

package interceptor import ( "context" "time" "entgo.io/ent" ) // SoftDeleteInterceptor automatically filters soft-deleted records func SoftDeleteInterceptor() ent.Interceptor { return ent.TraverseFunc(func(ctx context.Context, q ent.Query) error { // āϏāĻŦ query āϤ⧇ deleted_at IS NULL condition āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ if qr, ok := q.(interface { WhereP(...func(*sql.Selector)) }); ok { qr.WhereP( sql.FieldIsNull("deleted_at"), ) } return nil }) } // WithTrashed returns context that includes soft-deleted records func WithTrashed(ctx context.Context) context.Context { return context.WithValue(ctx, "include_trashed", true) }

Laravel āĻāϰ āϏāĻžāĻĨ⧇ āϤ⧁āϞāύāĻž:

// Laravel $posts = Post::all(); // Soft deleted āĻŦāĻžāĻĻ⧇ $posts = Post::withTrashed()->get(); // Soft deleted āϏāĻš $posts = Post::onlyTrashed()->get(); // āĻļ⧁āϧ⧁ soft deleted

Ent:

// Regular query - soft deleted āĻŦāĻžāĻĻ⧇ posts, _ := client.Post.Query().All(ctx) // With trashed - soft deleted āϏāĻš ctx = WithTrashed(ctx) posts, _ := client.Post.Query().All(ctx)

🔐 Privacy Policy - Global Scopes {#privacy-policy}

Laravel Global Scopes vs Ent Privacy

Privacy Policy āĻšāϞ⧋ Ent āĻāϰ āϏāĻŦāĻšā§‡āϝāĻŧ⧇ powerful feature - automatic access controlāĨ¤

┌──────────────────────────────────────────┐ │ Privacy Layer │ └──────────────────────────────────────────┘ Query/Mutation Request │ â–ŧ [Privacy Rules] │ ┌────┴────┐ │ │ Allow? Deny? │ │ â–ŧ â–ŧ Database Error

📊 Privacy Rules Types

Rule TypeāωāĻĻā§āĻĻ⧇āĻļā§āϝLaravel Equivalent
AllowMutationRuleMutation allow āĻ•āϰāĻžGate/Policy authorization
DenyMutationRuleMutation deny āĻ•āϰāĻžGate deny
AllowReadRuleRead access controlQuery scope with auth check
FilterRuleAutomatic filteringGlobal scope

✅ Privacy Policy āϕ⧇āύ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻŦ⧇āύ?

  1. Centralized Authorization - āĻāĻ• āϜāĻžāϝāĻŧāĻ—āĻžāϝāĻŧ āϏāĻŦ access control
  2. Type-Safe - Compile-time checking
  3. Automatic - Manual checking āĻāϰ āĻĻāϰāĻ•āĻžāϰ āύ⧇āχ
  4. Declarative - Clear āĻāĻŦāĻ‚ readable

📝 Step-by-Step: Privacy Policy Implementation

Step 1: Privacy Feature Enable āĻ•āϰ⧁āύ

# Generate code with privacy feature go run -mod=mod entgo.io/ent/cmd/ent generate --feature privacy ./ent/schema

Step 2: Basic Privacy Rules

File: ent/schema/post.go

package schema import ( "entgo.io/ent" "entgo.io/ent/schema" "entgo.io/ent/schema/field" "your-project/ent/privacy" "your-project/rule" ) type Post struct { ent.Schema } func (Post) Fields() []ent.Field { return []ent.Field{ field.String("title"), field.Text("content"), field.Int("author_id"), // Post āĻāϰ author field.Enum("status").Values("draft", "published"), } } // Policy of the Post schema func (Post) Policy() ent.Policy { return privacy.Policy{ Mutation: privacy.MutationPolicy{ // Rule 1: Admin users āϏāĻŦ āĻ•āĻŋāϛ⧁ āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇ rule.AllowIfAdmin(), // Rule 2: Author āĻļ⧁āϧ⧁ āύāĻŋāĻœā§‡āϰ post edit āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇ rule.AllowIfAuthor(), // Rule 3: āĻ…āĻ¨ā§āϝ āϕ⧇āω create āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇ āύāĻž privacy.DenyMutationOperationRule(ent.OpCreate), }, Query: privacy.QueryPolicy{ // Rule 1: Published posts āϏāĻŦāĻžāχ āĻĻ⧇āĻ–āϤ⧇ āĻĒāĻžāϰāĻŦ⧇ rule.AllowIfPublished(), // Rule 2: Draft posts āĻļ⧁āϧ⧁ author āĻĻ⧇āĻ–āϤ⧇ āĻĒāĻžāϰāĻŦ⧇ rule.FilterAuthorRule(), }, } }

Step 3: Privacy Rules Implementation

File: ent/rule/rule.go

package rule import ( "context" "entgo.io/ent" "entgo.io/ent/privacy" "your-project/ent/post" "your-project/ent/user" "your-project/ent/privacy" ) // Viewer context key type viewerKey struct{} // Viewer holds user information type Viewer struct { UserID int IsAdmin bool } // NewContext returns context with viewer func NewContext(ctx context.Context, v *Viewer) context.Context { return context.WithValue(ctx, viewerKey{}, v) } // FromContext returns viewer from context func FromContext(ctx context.Context) (*Viewer, bool) { v, ok := ctx.Value(viewerKey{}).(*Viewer) return v, ok } // AllowIfAdmin allows mutation if user is admin func AllowIfAdmin() privacy.MutationRule { return privacy.MutationRuleFunc(func(ctx context.Context, m ent.Mutation) error { viewer, ok := FromContext(ctx) if !ok { return privacy.Denyf("viewer not found in context") } if viewer.IsAdmin { return privacy.Allow // Admin access granted } return privacy.Skip // Try next rule }) } // AllowIfAuthor allows mutation if user is the author func AllowIfAuthor() privacy.MutationRule { return privacy.PostMutationRuleFunc(func(ctx context.Context, m *ent.PostMutation) error { viewer, ok := FromContext(ctx) if !ok { return privacy.Denyf("viewer not found in context") } // Check if updating/deleting if m.Op().Is(ent.OpUpdateOne | ent.OpDeleteOne) { // Get post author ID authorID, exists := m.AuthorID() if !exists { // If we're updating, get the current author id, _ := m.ID() post, err := m.Client().Post.Get(ctx, id) if err != nil { return privacy.Denyf("post not found") } authorID = post.AuthorID } // Check if viewer is the author if authorID == viewer.UserID { return privacy.Allow } } return privacy.Skip }) } // AllowIfPublished allows reading published posts func AllowIfPublished() privacy.QueryRule { return privacy.PostQueryRuleFunc(func(ctx context.Context, q *ent.PostQuery) error { // Add filter: only published posts q.Where(post.Status("published")) return privacy.Skip }) } // FilterAuthorRule filters posts by author for draft posts func FilterAuthorRule() privacy.QueryRule { return privacy.PostQueryRuleFunc(func(ctx context.Context, q *ent.PostQuery) error { viewer, ok := FromContext(ctx) if !ok { // No viewer, only show published return privacy.Skip } // Show published posts OR author's own posts q.Where( post.Or( post.Status("published"), post.AuthorID(viewer.UserID), ), ) return privacy.Skip }) }

Laravel Policy āĻāϰ āϏāĻžāĻĨ⧇ āϤ⧁āϞāύāĻž:

// Laravel Policy class PostPolicy { public function update(User $user, Post $post) { // Admin can update any post if ($user->is_admin) { return true; } // Author can update their own post return $user->id === $post->author_id; } public function view(User $user, Post $post) { // Published posts can be viewed by anyone if ($post->status === 'published') { return true; } // Draft posts only by author return $user->id === $post->author_id; } } // Controller public function update(Request $request, Post $post) { $this->authorize('update', $post); // Manual check $post->update($request->all()); } // Query scope class Post extends Model { protected static function booted() { static::addGlobalScope('published', function ($query) { if (!auth()->user()?->is_admin) { $query->where(function ($q) { $q->where('status', 'published') ->orWhere('author_id', auth()->id()); }); } }); } }

Step 4: Usage Example

package main import ( "context" "fmt" "your-project/ent" "your-project/ent/rule" ) func main() { client, _ := ent.Open("postgres", "...") defer client.Close() ctx := context.Background() // Scenario 1: Regular user trying to view posts regularUser := &rule.Viewer{UserID: 1, IsAdmin: false} ctx = rule.NewContext(ctx, regularUser) // āĻāχ query āĻļ⧁āϧ⧁ published posts āĻāĻŦāĻ‚ user 1 āĻāϰ āύāĻŋāĻœā§‡āϰ posts āĻĻ⧇āĻ–āĻžāĻŦ⧇ posts, err := client.Post.Query().All(ctx) if err != nil { fmt.Println("Error:", err) } fmt.Println("Regular user sees:", len(posts), "posts") // Scenario 2: Admin user trying to view posts adminUser := &rule.Viewer{UserID: 2, IsAdmin: true} ctx = rule.NewContext(ctx, adminUser) // Admin āϏāĻŦ posts āĻĻ⧇āĻ–āϤ⧇ āĻĒāĻžāϰāĻŦ⧇ posts, _ = client.Post.Query().All(ctx) fmt.Println("Admin sees:", len(posts), "posts") // Scenario 3: Update post regularUser = &rule.Viewer{UserID: 1, IsAdmin: false} ctx = rule.NewContext(ctx, regularUser) // User 1 āĻļ⧁āϧ⧁ āύāĻŋāĻœā§‡āϰ post update āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇ _, err = client.Post.UpdateOneID(1). SetTitle("Updated Title"). Save(ctx) if err != nil { fmt.Println("Update failed:", err) // Permission denied } // Scenario 4: Admin update any post adminUser = &rule.Viewer{UserID: 2, IsAdmin: true} ctx = rule.NewContext(ctx, adminUser) // Admin āϝ⧇āϕ⧋āύ⧋ post update āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇ _, err = client.Post.UpdateOneID(1). SetTitle("Admin Updated"). Save(ctx) if err == nil { fmt.Println("Admin update successful") } }

🚀 Step-by-Step Professional Implementation {#professional-implementation}

āĻāĻ–āύ āĻāĻ•āϟāĻŋ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ professional blog application āϤ⧈āϰāĻŋ āĻ•āϰāĻŦā§‹ āϝāĻžāϤ⧇ āϏāĻŦ features āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤

Project Structure

your-project/ ├── ent/ │ ├── schema/ │ │ ├── mixin/ │ │ │ ├── time_mixin.go │ │ │ └── soft_delete_mixin.go │ │ ├── user.go │ │ ├── post.go │ │ └── comment.go │ ├── rule/ │ │ └── rule.go │ └── interceptor/ │ ├── tenant.go │ └── audit.go ├── main.go └── go.mod

Step 1: Initialize Project

# Project āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ mkdir blog-app && cd blog-app go mod init blog-app # Ent install āĻ•āϰ⧁āύ go get entgo.io/ent/cmd/ent # Schema initialize āĻ•āϰ⧁āύ go run entgo.io/ent/cmd/ent init User Post Comment

Step 2: Create Mixins

File: ent/schema/mixin/time_mixin.go

package mixin import ( "time" "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" ) type TimeMixin struct { mixin.Schema } func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time("created_at"). Immutable(). Default(time.Now). Comment("Record creation timestamp"), field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now). Comment("Record last update timestamp"), } }

File: ent/schema/mixin/soft_delete_mixin.go

package mixin import ( "time" "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" ) type SoftDeleteMixin struct { mixin.Schema } func (SoftDeleteMixin) Fields() []ent.Field { return []ent.Field{ field.Time("deleted_at"). Optional(). Nillable(). Comment("Soft delete timestamp"), } }

Step 3: Define Schemas with Relationships

File: ent/schema/user.go

package schema import ( "context" "strings" "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" "golang.org/x/crypto/bcrypt" "blog-app/ent/hook" genmixin "blog-app/ent/schema/mixin" ) type User struct { ent.Schema } func (User) Mixin() []ent.Mixin { return []ent.Mixin{ genmixin.TimeMixin{}, genmixin.SoftDeleteMixin{}, } } func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). NotEmpty(). Comment("User full name"), field.String("email"). Unique(). NotEmpty(). Comment("User email address"), field.String("password"). Sensitive(). // Password hidden from logs NotEmpty(). Comment("Hashed password"), field.Bool("is_admin"). Default(false). Comment("Admin flag"), field.Int("tenant_id"). Optional(). Comment("Tenant ID for multi-tenancy"), } } func (User) Edges() []ent.Edge { return []ent.Edge{ // User has many posts edge.To("posts", Post.Type). Comment("Posts authored by user"), // User has many comments edge.To("comments", Comment.Type). Comment("Comments made by user"), } } func (User) Hooks() []ent.Hook { return []ent.Hook{ // Email lowercase āĻāĻŦāĻ‚ password hash hook.On( func(next ent.Mutator) ent.Mutator { return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) { // Email normalize if email, ok := m.Email(); ok { m.SetEmail(strings.ToLower(strings.TrimSpace(email))) } // Password hash if password, ok := m.Password(); ok { hashed, err := bcrypt.GenerateFromPassword( []byte(password), bcrypt.DefaultCost, ) if err != nil { return nil, err } m.SetPassword(string(hashed)) } return next.Mutate(ctx, m) }) }, ent.OpCreate|ent.OpUpdateOne, ), } }

File: ent/schema/post.go

package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" genmixin "blog-app/ent/schema/mixin" ) type Post struct { ent.Schema } func (Post) Mixin() []ent.Mixin { return []ent.Mixin{ genmixin.TimeMixin{}, genmixin.SoftDeleteMixin{}, } } func (Post) Fields() []ent.Field { return []ent.Field{ field.String("title"). NotEmpty(). MaxLen(200). Comment("Post title"), field.String("slug"). Unique(). NotEmpty(). Comment("URL-friendly slug"), field.Text("content"). NotEmpty(). Comment("Post content"), field.Enum("status"). Values("draft", "published", "archived"). Default("draft"). Comment("Post status"), field.Int("author_id"). Comment("Author user ID"), field.Int("tenant_id"). Optional(). Comment("Tenant ID"), field.Int("view_count"). Default(0). NonNegative(). Comment("Number of views"), } } func (Post) Edges() []ent.Edge { return []ent.Edge{ // Post belongs to author (user) edge.From("author", User.Type). Ref("posts"). Field("author_id"). Unique(). Required(), // Post has many comments edge.To("comments", Comment.Type), } } func (Post) Indexes() []ent.Index { return []ent.Index{ // Index on status for faster filtering index.Fields("status"), // Composite index for tenant queries index.Fields("tenant_id", "status"), // Unique slug per tenant index.Fields("tenant_id", "slug").Unique(), } }

File: ent/schema/comment.go

package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" genmixin "blog-app/ent/schema/mixin" ) type Comment struct { ent.Schema } func (Comment) Mixin() []ent.Mixin { return []ent.Mixin{ genmixin.TimeMixin{}, genmixin.SoftDeleteMixin{}, } } func (Comment) Fields() []ent.Field { return []ent.Field{ field.Text("content"). NotEmpty(). Comment("Comment content"), field.Int("post_id"). Comment("Associated post ID"), field.Int("user_id"). Comment("Comment author ID"), field.Bool("is_approved"). Default(false). Comment("Moderation flag"), } } func (Comment) Edges() []ent.Edge { return []ent.Edge{ // Comment belongs to post edge.From("post", Post.Type). Ref("comments"). Field("post_id"). Unique(). Required(), // Comment belongs to user edge.From("user", User.Type). Ref("comments"). Field("user_id"). Unique(). Required(), } }

Step 4: Generate Code

# Code generate āĻ•āϰ⧁āύ (without privacy first) go generate ./ent # Privacy āϏāĻš generate āĻ•āϰāϤ⧇ āϚāĻžāχāϞ⧇ go run -mod=mod entgo.io/ent/cmd/ent generate --feature privacy ./ent/schema

Step 5: Create Complete Application

File: main.go

package main import ( "context" "fmt" "log" "blog-app/ent" "blog-app/ent/post" "blog-app/ent/user" _ "github.com/mattn/go-sqlite3" ) func main() { // Database connection client, err := ent.Open("sqlite3", "file:blog.db?cache=shared&_fk=1") if err != nil { log.Fatalf("failed opening connection: %v", err) } defer client.Close() // Run migration ctx := context.Background() if err := client.Schema.Create(ctx); err != nil { log.Fatalf("failed creating schema: %v", err) } // Create sample data if err := createSampleData(ctx, client); err != nil { log.Fatal(err) } // Query examples queryExamples(ctx, client) } func createSampleData(ctx context.Context, client *ent.Client) error { // Create users admin, err := client.User.Create(). SetName("Admin User"). SetEmail("admin@example.com"). SetPassword("password123"). SetIsAdmin(true). Save(ctx) if err != nil { return fmt.Errorf("creating admin: %w", err) } author, err := client.User.Create(). SetName("John Doe"). SetEmail("john@example.com"). SetPassword("password123"). Save(ctx) if err != nil { return fmt.Errorf("creating author: %w", err) } // Create posts post1, err := client.Post.Create(). SetTitle("Getting Started with Ent"). SetSlug("getting-started-ent"). SetContent("This is a comprehensive guide..."). SetStatus("published"). SetAuthor(author). Save(ctx) if err != nil { return fmt.Errorf("creating post: %w", err) } post2, err := client.Post.Create(). SetTitle("Advanced Ent Patterns"). SetSlug("advanced-ent-patterns"). SetContent("Learn advanced patterns..."). SetStatus("draft"). SetAuthor(author). Save(ctx) if err != nil { return fmt.Errorf("creating post: %w", err) } // Create comments _, err = client.Comment.Create(). SetContent("Great article!"). SetPost(post1). SetUser(admin). SetIsApproved(true). Save(ctx) if err != nil { return fmt.Errorf("creating comment: %w", err) } fmt.Println("✅ Sample data created successfully!") return nil } func queryExamples(ctx context.Context, client *ent.Client) { fmt.Println("\n📊 Query Examples:") fmt.Println("==================") // Example 1: Get all published posts with author fmt.Println("\n1. Published posts with authors:") posts, err := client.Post.Query(). Where(post.Status("published")). WithAuthor(). // Eager load author All(ctx) if err != nil { log.Fatal(err) } for _, p := range posts { author := p.Edges.Author fmt.Printf(" - %s by %s (views: %d)\n", p.Title, author.Name, p.ViewCount) } // Example 2: Get user with all their posts fmt.Println("\n2. Author with all posts:") authors, err := client.User.Query(). Where(user.IsAdmin(false)). WithPosts(). // Eager load posts All(ctx) if err != nil { log.Fatal(err) } for _, author := range authors { fmt.Printf(" - %s has %d posts\n", author.Name, len(author.Edges.Posts)) for _, p := range author.Edges.Posts { fmt.Printf(" * %s (%s)\n", p.Title, p.Status) } } // Example 3: Complex query with joins fmt.Println("\n3. Posts with comment count:") type PostWithCommentCount struct { ID int Title string CommentCount int } var results []PostWithCommentCount err = client.Post.Query(). Where(post.Status("published")). Modify(func(s *sql.Selector) { t := sql.Table("comments") s.LeftJoin(t).On(s.C("id"), t.C("post_id")) s.GroupBy(s.C("id"), s.C("title")) s.Select( s.C("id"), s.C("title"), sql.As(sql.Count("*"), "comment_count"), ) }). Scan(ctx, &results) if err != nil { log.Fatal(err) } for _, r := range results { fmt.Printf(" - %s (%d comments)\n", r.Title, r.CommentCount) } }

Step 6: Run Application

# Dependencies install āĻ•āϰ⧁āύ go mod tidy # Application run āĻ•āϰ⧁āύ go run main.go

📚 Laravel āĻĨ⧇āϕ⧇ Ent Migration Cheat Sheet

Common Patterns

TaskLaravel EloquentEnt Framework
Model Definitionclass User extends Modeltype User struct { ent.Schema }
Timestamps$timestamps = truemixin.TimeMixin{}
Soft Deletesuse SoftDeletesmixin.SoftDeleteMixin{}
CreatingUser::create([...])client.User.Create().Set...().Save(ctx)
FindingUser::find(1)client.User.Get(ctx, 1)
Updating$user->update([...])user.Update().Set...().Save(ctx)
Deleting$user->delete()client.User.DeleteOne(user).Exec(ctx)
Query BuilderUser::where('active', true)client.User.Query().Where(user.Active(true))
Relationships$user->postsuser.QueryPosts().All(ctx)
Eager LoadingUser::with('posts')client.User.Query().WithPosts()
ObserversUserObserverSchema Hooks()
ScopesscopeActive($query)Custom query method
Global ScopesaddGlobalScope(...)Interceptor
PoliciesPostPolicyPrivacy Policy()
EventsEvent::listen(...)Hooks

đŸŽ¯ Best Practices Checklist

✅ Mixins

  • TimeMixin āϏāĻŦ schemas āĻ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ
  • SoftDeleteMixin āĻĒā§āϰāϝāĻŧā§‹āϜāύ⧀āϝāĻŧ schemas āĻ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ
  • Custom mixins āϤ⧈āϰāĻŋ āĻ•āϰ⧇ āϕ⧋āĻĄ duplication āĻāĻĄāĻŧāĻŋāϝāĻŧ⧇ āϚāϞ⧁āύ
  • Mixin āĻ hooks āĻāĻŦāĻ‚ indexes āϝ⧁āĻ•ā§āϤ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āύ

✅ Hooks

  • Validation hooks āϏāĻŦāĻžāϰ āφāϗ⧇ āϰāĻžāϖ⧁āύ
  • Side effects (email, logs) āϏāĻŦāĻžāϰ āĻļ⧇āώ⧇ āϰāĻžāϖ⧁āύ
  • Hook āĻ database query āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āϏāĻžāĻŦāϧāĻžāύ āĻĨāĻžāϕ⧁āύ (infinite loop)
  • Error handling āϏāĻ āĻŋāĻ•āĻ­āĻžāĻŦ⧇ āĻ•āϰ⧁āύ

✅ Interceptors

  • Tenant isolation interceptor āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ (multi-tenant app)
  • Audit logging interceptor āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ
  • Performance monitoring interceptor consider āĻ•āϰ⧁āύ
  • Interceptor chain āĻāϰ order āĻŽāύ⧇ āϰāĻžāϖ⧁āύ

✅ Privacy

  • āϏāĻŦ user-facing schemas āĻ privacy policy āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ
  • Admin bypass rule āϏāĻŦāĻžāϰ āφāϗ⧇ āϰāĻžāϖ⧁āύ
  • Least privilege principle follow āĻ•āϰ⧁āύ
  • Test cases āϞāĻŋāϖ⧁āύ privacy rules āĻāϰ āϜāĻ¨ā§āϝ

✅ Performance

  • Eager loading āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ (N+1 query problem āĻāĻĄāĻŧāĻžāύ)
  • Appropriate indexes āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ
  • Query optimize āĻ•āϰ⧁āύ
  • Connection pooling configure āĻ•āϰ⧁āύ

đŸ”Ĩ Advanced Tips

1. Custom Query Helpers đŸ”Ĩ

// Helper methods āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ generated code āĻ // ent/post_query.go func (pq *PostQuery) Published() *PostQuery { return pq.Where(post.Status("published")) } func (pq *PostQuery) ByAuthor(authorID int) *PostQuery { return pq.Where(post.AuthorID(authorID)) } func (pq *PostQuery) Recent(limit int) *PostQuery { return pq.Order(ent.Desc(post.FieldCreatedAt)).Limit(limit) } // Usage posts, _ := client.Post.Query(). Published(). Recent(10). All(ctx)

2. Transaction Management

// Laravel DB::transaction() āĻāϰ āĻŽāϤ⧋ func createPostWithTags(ctx context.Context, client *ent.Client) error { tx, err := client.Tx(ctx) if err != nil { return err } // Rollback function defer func() { if v := recover(); v != nil { tx.Rollback() panic(v) } }() // Create post post, err := tx.Post.Create(). SetTitle("New Post"). SetContent("Content"). Save(ctx) if err != nil { return rollback(tx, err) } // Create tags for _, tagName := range []string{"golang", "ent"} { _, err := tx.Tag.Create(). SetName(tagName). AddPosts(post). Save(ctx) if err != nil { return rollback(tx, err) } } // Commit transaction return tx.Commit() } func rollback(tx *ent.Tx, err error) error { if rerr := tx.Rollback(); rerr != nil { err = fmt.Errorf("%w: %v", err, rerr) } return err }

3. Pagination

// Laravel paginate() āĻāϰ āĻŽāϤ⧋ func getPaginatedPosts(ctx context.Context, client *ent.Client, page, perPage int) (*PaginatedResult, error) { offset := (page - 1) * perPage // Count total total, err := client.Post.Query(). Where(post.Status("published")). Count(ctx) if err != nil { return nil, err } // Get paginated data posts, err := client.Post.Query(). Where(post.Status("published")). Offset(offset). Limit(perPage). Order(ent.Desc(post.FieldCreatedAt)). All(ctx) if err != nil { return nil, err } return &PaginatedResult{ Data: posts, Total: total, Page: page, PerPage: perPage, TotalPages: (total + perPage - 1) / perPage, }, nil } type PaginatedResult struct { Data []*ent.Post Total int Page int PerPage int TotalPages int }

📖 āϏāĻžāϰāϏāĻ‚āĻ•ā§āώ⧇āĻĒ

āĻāχ guide āĻ āφāĻŽāϰāĻž āĻļāĻŋāĻ–āϞāĻžāĻŽ:

  1. Mixins - āϕ⧋āĻĄ reuse āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ (Laravel Traits āĻāϰ āĻŽāϤ⧋)
  2. Hooks - Lifecycle events handle āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ (Laravel Observers āĻāϰ āĻŽāϤ⧋)
  3. Interceptors - Query modify āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ (Laravel Query Scopes/Global Scopes)
  4. Privacy Policy - Access control āĻāϰ āϜāĻ¨ā§āϝ (Laravel Policies)

Laravel Eloquent āĻāϰ āϏāĻžāĻĨ⧇ Ent Framework āĻāϰ āĻŽā§‚āϞ āĻĒāĻžāĻ°ā§āĻĨāĻ•ā§āϝ:

  • Type-safe: Compile-time error checking
  • Explicit: āϏāĻŦ āĻ•āĻŋāϛ⧁ explicit āĻ­āĻžāĻŦ⧇ define āĻ•āϰāϤ⧇ āĻšāϝāĻŧ
  • Code generation: Schema āĻĨ⧇āϕ⧇ code generate āĻšāϝāĻŧ
  • Performance: Optimized queries āĻāĻŦāĻ‚ better performance

🤝 āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āĻĒāĻĻāĻ•ā§āώ⧇āĻĒ

  1. Official documentation āĻĒāĻĄāĻŧ⧁āύ: https://entgo.io 
  2. Example projects āĻĻ⧇āϖ⧁āύ: https://github.com/ent/ent/tree/master/examples 
  3. āφāĻĒāύāĻžāϰ existing Laravel project āϕ⧇ Ent āĻ migrate āĻ•āϰāĻžāϰ planning āĻ•āϰ⧁āύ
  4. Testing āĻāĻŦāĻ‚ Mocking āύāĻŋāϝāĻŧ⧇ āφāϰāĻ“ āϜāĻžāύ⧁āύ

āφāĻļāĻž āĻ•āϰāĻŋ āĻāχ guide āφāĻĒāύāĻžāϰ Laravel āĻĨ⧇āϕ⧇ Ent Framework āĻ transition āϏāĻšāϜ āĻ•āϰāĻŦ⧇! 🚀