Laravel Eloquent āĻĨā§āĻā§ Ent Framework āĻ Migration
đ āϏā§āĻāĻŋāĻĒāϤā§āϰ
- Laravel vs Ent - āĻĻā§āϰā§āϤ āϤā§āϞāύāĻž
- Mixins - āĻā§āĻĄ āĻĒā§āύāϰā§āĻŦā§āϝāĻŦāĻšāĻžāϰ
- Hooks - Event Handling
- Interceptors - Query Modification
- Privacy Policy - Global Scopes
- Step-by-Step Professional Implementation
đ Laravel vs Ent - āĻĻā§āϰā§āϤ āϤā§āϞāύāĻž {#laravel-vs-ent-comparison}
Laravel Eloquent āĻāĻŦāĻ Ent Framework āĻāϰ āĻŽāϧā§āϝ⧠āĻŽā§āϞ āĻĒāĻžāϰā§āĻĨāĻā§āϝ āĻāĻŦāĻ āϏāĻžāĻĻā§āĻļā§āϝ:
| Laravel Eloquent | Ent Framework | āĻŦā§āϝāĻžāĻā§āϝāĻž |
|---|---|---|
| Traits | Mixins | āĻā§āĻĄ āĻĒā§āύāϰā§āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰāĻžāϰ āĻāύā§āϝ |
| Observers/Events | Hooks | Model lifecycle events handle āĻāϰāĻžāϰ āĻāύā§āϝ |
| Query Scopes | Interceptors | Query modify āĻāϰāĻžāϰ āĻāύā§āϝ |
| Global Scopes | Privacy Policy | āϏāĻŦ query āϤ⧠automatic filter apply āĻāϰāĻžāϰ āĻāύā§āϝ |
creating, created | OnCreate hook | āύāϤā§āύ entry āϤā§āϰāĻŋāϰ āϏāĻŽāϝāĻŧ |
updating, updated | OnUpdate hook | entry update āĻāϰāĻžāϰ āϏāĻŽāϝāĻŧ |
deleting, deleted | OnDelete hook | entry delete āĻāϰāĻžāϰ āϏāĻŽāϝāĻŧ |
SoftDeletes trait | Mixin with deleted_at | Soft 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/mixinStep 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 Event | Ent Hook | āĻāĻāύ Execute āĻšāϝāĻŧ |
|---|---|---|
creating | N/A (use default) | Before save, āĻā§āϝāĻžāϞāĻŋāĻĄā§āĻļāύā§āϰ āĻāĻā§ |
created | OnCreate return | After save successful |
updating | N/A (use mutation) | Before update |
updated | OnUpdate return | After update successful |
deleting | N/A | Before delete |
deleted | OnDelete return | After delete successful |
saving | Mutation builder | Before any save |
saved | Hook return | After any save |
â Hook Types in Ent
Ent āĻ ā§¨ āϧāϰāύā§āϰ hooks āĻāĻā§:
- Schema Hooks - āύāĻŋāϰā§āĻĻāĻŋāώā§āĻ schema āϰ āĻāύā§āϝ
- 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 āϤā§āϞāύāĻž
| Laravel | Ent | āĻāĻĻā§āĻĻā§āĻļā§āϝ |
|---|---|---|
| Local Scope | Query Helper | Reusable query conditions |
| Global Scope | Interceptor | Automatic query filtering |
| Dynamic Scope | Interceptor Chain | Runtime query modification |
â Interceptor Types
- Traverse Interceptors - Query modify āĻāϰāĻžāϰ āĻāύā§āϝ
- Query Interceptors - Query execution intercept āĻāϰāĻžāϰ āĻāύā§āϝ
- 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 deletedEnt:
// 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 |
|---|---|---|
AllowMutationRule | Mutation allow āĻāϰāĻž | Gate/Policy authorization |
DenyMutationRule | Mutation deny āĻāϰāĻž | Gate deny |
AllowReadRule | Read access control | Query scope with auth check |
FilterRule | Automatic filtering | Global scope |
â Privacy Policy āĻā§āύ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰāĻŦā§āύ?
- Centralized Authorization - āĻāĻ āĻāĻžāϝāĻŧāĻāĻžāϝāĻŧ āϏāĻŦ access control
- Type-Safe - Compile-time checking
- Automatic - Manual checking āĻāϰ āĻĻāϰāĻāĻžāϰ āύā§āĻ
- 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/schemaStep 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.modStep 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 CommentStep 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/schemaStep 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
| Task | Laravel Eloquent | Ent Framework |
|---|---|---|
| Model Definition | class User extends Model | type User struct { ent.Schema } |
| Timestamps | $timestamps = true | mixin.TimeMixin{} |
| Soft Deletes | use SoftDeletes | mixin.SoftDeleteMixin{} |
| Creating | User::create([...]) | client.User.Create().Set...().Save(ctx) |
| Finding | User::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 Builder | User::where('active', true) | client.User.Query().Where(user.Active(true)) |
| Relationships | $user->posts | user.QueryPosts().All(ctx) |
| Eager Loading | User::with('posts') | client.User.Query().WithPosts() |
| Observers | UserObserver | Schema Hooks() |
| Scopes | scopeActive($query) | Custom query method |
| Global Scopes | addGlobalScope(...) | Interceptor |
| Policies | PostPolicy | Privacy Policy() |
| Events | Event::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 āĻ āĻāĻŽāϰāĻž āĻļāĻŋāĻāϞāĻžāĻŽ:
- Mixins - āĻā§āĻĄ reuse āĻāϰāĻžāϰ āĻāύā§āϝ (Laravel Traits āĻāϰ āĻŽāϤā§)
- Hooks - Lifecycle events handle āĻāϰāĻžāϰ āĻāύā§āϝ (Laravel Observers āĻāϰ āĻŽāϤā§)
- Interceptors - Query modify āĻāϰāĻžāϰ āĻāύā§āϝ (Laravel Query Scopes/Global Scopes)
- 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
đ¤ āĻĒāϰāĻŦāϰā§āϤ⧠āĻĒāĻĻāĻā§āώā§āĻĒ
- Official documentation āĻĒāĻĄāĻŧā§āύ: https://entgo.ioÂ
- Example projects āĻĻā§āĻā§āύ: https://github.com/ent/ent/tree/master/examplesÂ
- āĻāĻĒāύāĻžāϰ existing Laravel project āĻā§ Ent āĻ migrate āĻāϰāĻžāϰ planning āĻāϰā§āύ
- Testing āĻāĻŦāĻ Mocking āύāĻŋāϝāĻŧā§ āĻāϰāĻ āĻāĻžāύā§āύ
āĻāĻļāĻž āĻāϰāĻŋ āĻāĻ guide āĻāĻĒāύāĻžāϰ Laravel āĻĨā§āĻā§ Ent Framework āĻ transition āϏāĻšāĻ āĻāϰāĻŦā§! đ