1. معرفی ent

Ent یک چارچوب موجودیت است که توسط فیس‌بوک به‌طور خاص برای زبان Go توسعه یافته است. این چارچوب فرآیند ساخت و نگهداری برنامه‌های مدل داده بزرگ را ساده‌تر می‌کند. اصول اصلی ent عمدتاً به موارد زیر پیروی می‌کند:

  • به راحتی مدل طرح پایگاه داده را به‌عنوان یک ساختار گراف بیان می‌کند.
  • طرح را به‌صورت کدهای زبان Go تعریف می‌کند.
  • انواع استاتیک را بر اساس تولید کدها پیاده‌سازی می‌کند.
  • نوشتن پرس‌وجوهای پایگاه داده و گردش گراف بسیار ساده است.
  • با استفاده از الگوهای زبان Go به‌راحتی گسترش یافته و سفارشی‌سازی می‌شود.

2. تنظیمات محیط

برای شروع استفاده از چارچوب ent، اطمینان حاصل کنید که زبان Go در محیط توسعه شما نصب شده باشد.

اگر دایرکتوری پروژه شما خارج از GOPATH است، یا اگر با GOPATH آشنا نیستید، می‌توانید از دستور زیر برای ایجاد یک پروژه ماژول Go جدید استفاده کنید:

go mod init entdemo

این دستور یک ماژول Go جدید را مقدماتی کرده و یک فایل go.mod جدید برای پروژه entdemo شما ایجاد می‌کند.

3. تعریف اولین طرح

3.1. ایجاد طرح با استفاده از ابزار ent CLI

ابتدا، باید دستور زیر را در دایرکتوری اصلی پروژه خود اجرا کنید تا طرحی با نام User با استفاده از ابزار ent CLI ایجاد شود:

go run -mod=mod entgo.io/ent/cmd/ent new User

دستور فوق ساختار User را در دایرکتوری entdemo/ent/schema/ ایجاد می‌کند:

فایل entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

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

// Fields of the User.
func (User) Fields() []ent.Field {
    return nil
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return nil
}

3.2. افزودن فیلدها

بعد، باید تعریف‌های فیلد را به طرح User اضافه کنیم. در زیر نمونه‌ای از اضافه کردن دو فیلد به موجودیت User آمده است.

فایل تغییر یافته entdemo/ent/schema/user.go:

package schema

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

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

این قطعه کد دو فیلد برای مدل User تعریف می‌کند: age و name، جایی که age یک عدد صحیح مثبت و name یک رشته با مقدار پیش‌فرض "unknown" است.

3.3. تولید موجودیت‌های پایگاه داده

پس از تعریف طرح، باید دستور go generate را اجرا کنید تا منطق دسترسی پایگاه داده زیرین تولید شود.

دستور زیر را در دایرکتوری اصلی پروژه خود اجرا کنید:

go generate ./ent

این دستور براساس طرح تعریف شده از پیش، کدهای Go مربوطه را تولید می‌کند که به ساختار فایل زیر منجر می‌شود:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (چندین فایل برای اختصار حذف شده است)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. ایجاد اتصال به پایگاه داده

برای ایجاد اتصال به پایگاه داده MySQL، می‌توانیم از تابع Open ارائه شده توسط چارچوب ent استفاده کنیم. ابتدا درایور MySQL را وارد کرده و سپس رشته اتصال صحیح را فراهم کرده و اتصال پایگاه داده را مقدم کنیم.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // وارد کردن درایور MySQL
)

func main() {
    // از ent.Open برای برقراری اتصال با پایگاه داده MySQL استفاده کنید.
    // به یاد داشته باشید که جایگزین کردن مقبول "your_username"، "your_password" و "your_database" زیر.
    client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
    if err != nil {
        log.Fatalf("خطا در باز کردن اتصال به mysql: %v", err)
    }
    defer client.Close()

    // ابزار مهاجرت خودکار را اجرا کنید
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("خطا در ایجاد منابع طرح: %v", err)
    }
    
    // می‌توانید منطق تجاری اضافی را در اینجا بنویسید
}

4.2. ایجاد موجودیت‌ها

ایجاد یک موجودیت کاربر شامل ساخت یک شیء موجودیت جدید و ثبت آن در پایگاه داده با استفاده از متد Save یا SaveX است. کد زیر نحوه ایجاد یک موجودیت کاربر جدید و مقدم کردن دو فیلد سن و نام را نشان می‌دهد.

// تابع CreateUser برای ایجاد یک موجودیت کاربر جدید استفاده می‌شود
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // از client.User.Create() برای ساخت درخواست برای ایجاد یک کاربر استفاده کنید.
    // سپس متد‌های SetAge و SetName را برای تنظیم مقادیر فیلدهای موجودیت به آن اتصال دهید.
    u, err := client.User.
        Create().
        SetAge(30).    // تنظیم سن کاربر
        SetName("سید"). // تنظیم نام کاربر
        Save(ctx)     // فراخوانی Save برای ذخیره موجودیت در پایگاه داده
    if err != nil {
        return nil, fmt.Errorf("خطا در ایجاد کاربر: %w", err)
    }
    log.Println("کاربر ایجاد شد: ", u)
    return u, nil
}

در تابع main، می‌توانید تابع CreateUser را صدا بزنید تا موجودیت کاربر جدید ایجاد کنید.

func main() {
    // ... کد برقراری اتصال پایگاه داده حذف شده است

    // ایجاد یک موجودیت کاربر
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("خطا در ایجاد کاربر: %v", err)
    }
    log.Printf("کاربر ایجاد شد: %#v\n", u)
}

4.3. پرس و جوی موجودیت‌ها

برای پرس و جوی موجودیت‌ها می‌توانیم از سازنده پرس و جوی توسط ent تولید شده استفاده کنیم. کد زیر نحوه پرس و جوی یک کاربر به نام "سید" را نشان می‌دهد.

// تابع QueryUser برای پرس و جوی موجودیت کاربر با یک نام مشخص استفاده می‌شود
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // از client.User.Query() برای ساخت پرس و جوی برای User استفاده کنید.
    // سپس متد Where را برای اضافه کردن شرایط پرس و جو، مانند پرس و جو بر اساس نام کاربر
    u, err := client.User.
        Query().
        Where(user.NameEQ("سید")).      // اضافه کردن شرط پرس و جو، در این حالت، نام "سید" است
        Only(ctx)                      // متد Only نشان می‌دهد که تنها یک نتیجه انتظار داریم
    if err != nil {
        return nil, fmt.Errorf("خطا در پرس و جوی کاربر: %w", err)
    }
    log.Println("کاربر بازگشتی: ", u)
    return u, nil
}

در تابع main، می‌توانید تابع QueryUser را صدا بزنید تا موجودیت کاربر را پرس و جو کنید.

func main() {
    // ... کد برقراری اتصال پایگاه داده و ایجاد کاربر حذف شده است

    // پرس و جوی موجودیت کاربر
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("خطا در پرس و جوی کاربر: %v", err)
    }
    log.Printf("کاربر پرس و جو شد: %#v\n", u)
}

5.1. درک یال‌ها و یال‌های معکوس

در چارچوب ent، مدل داده به عنوان یک ساختار گرافیکی نشان داده می‌شود، جایی که انتیتی‌ها نودها در گراف را نمایش می‌دهند و روابط بین انتیتی‌ها توسط یال‌ها نمایش داده می‌شود. یک یال یک اتصال از یک انتیتی به یک دیگری است، به عنوان مثال یک کاربر می‌تواند چندین ماشین را داشته باشد.

یال‌های معکوس ارجاعات معکوس به یال‌ها هستند که به صورت منطقی رابطه معکوس بین انتیتی‌ها را نشان می‌دهند، اما رابطه جدیدی را در پایگاه داده ایجاد نمی‌کنند. به عنوان مثال، از طریق یال معکوس یک ماشین، می‌توانیم کاربری را که این ماشین را مالک است پیدا کنیم.

اهمیت اصلی یال‌ها و یال‌های معکوس در این است که مسیر یابی بین انتیتی‌های مرتبط بسیار شفاف و آسان را فراهم می‌کنند.

نکته: در ent، یال‌ها متناظر با کلید‌های خارجی پایگاه داده سنتی هستند و برای تعیین رابطه بین جداول استفاده می‌شوند.

5.2. تعریف یال‌ها در اسکیما

ابتدا، از دستورالعمل CLI ent برای ایجاد اسکیمای اولیه برای ماشین و گروه استفاده می‌کنیم:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

سپس، در اسکیمای کاربر، یال با ماشین را برای نشان دادن رابطه بین کاربران و ماشین‌ها تعریف می‌کنیم. ما می‌توانیم یک یال به نام ماشین‌ها را به نوع ماشین در انتیتی کاربر اضافه کنیم و نشان دهیم که یک کاربر می‌تواند چندین ماشین داشته باشد:

// entdemo/ent/schema/user.go

// یال‌های کاربر.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

پس از تعریف یال‌ها، ما باید دوباره go generate ./ent را اجرا کنیم تا کد متناظر تولید شود.

5.3. عملیات بر روی داده‌های یال

ساختن ماشین‌های مرتبط با یک کاربر یک فرآیند ساده است. با داشتن انتیتی کاربر، ما می‌توانیم یک انتیتی ماشین جدید ایجاد کرده و آن را با کاربر مرتبط کنیم:

import (
    "context"
    "log"
    "entdemo/ent"
    // اطمینان حاصل کنید که تعریف اسکیما برای ماشین را وارد کرده باشید
    _ "entdemo/ent/schema"
)

func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("دریافت کاربر ناموفق بود: %v", err)
        return err
    }

    // یک ماشین جدید ایجاد کرده و آن را با کاربر مرتبط می‌کنیم
    _, err = client.Car.
        Create().
        SetModel("تسلا").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("ایجاد ماشین برای کاربر ناموفق بود: %v", err)
        return err
    }

    log.Println("ماشین ایجاد شد و با کاربر مرتبط شد")
    return nil
}

به همین ترتیب، پرس‌وجوی ماشین‌های یک کاربر نیز ساده است. اگر بخواهیم یک لیست از تمام ماشین‌های متعلق به یک کاربر را بازیابی کنیم، می‌توانیم موارد زیر را انجام دهیم:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("دریافت کاربر ناموفق بود: %v", err)
        return err
    }

    // پرس‌وجوی تمام ماشین‌های متعلق به کاربر
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("پرس‌وجوی ماشین‌ها ناموفق بود: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("ماشین: %v، مدل: %v", car.ID, car.Model)
    }
    return nil
}

توسط مراحل فوق، ما نه تنها یاد گرفتیم چگونه یال‌ها را در اسکیما تعریف کنیم، بلکه نشان دادیم چگونه اطلاعات مرتبط با یال‌ها را ایجاد و پرس‌وجو کنیم.

6. پیاده‌سازی و پرس‌وجوی گراف

6.1. درک ساختارهای گراف

در ent، ساختارهای گراف توسط انتیتی‌ها و یال‌های بین آن‌ها نمایش داده می‌شود. هر انتیتی معادل یک نود در گراف است و روابط بین انتیتی‌ها توسط یال‌ها نمایش داده می‌شود که می‌تواند یک به یک، یک به چند، چند به یک و ... باشد. این ساختار گراف باعث می‌شود که پرس‌وجوها و عملیات پیچیده بر روی یک پایگاه داده رابطه‌ای ساده و شفاف باشد.

6.2. گذر از ساختارهای گرافی

نوشتن کد گذر از گراف اصولاً شامل پرس و جو و ارتباط داده ها از طریق یال های بین موجودیت ها است. در زیر یک مثال ساده از نحوه گذر از ساختار گراف در ent آمده است:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal یک مثال از گذر از ساختار گراف است
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // پرس و جو برای یافتن کاربر به نام "آریل"
    a8m, err := client.User.Query().Where(user.NameEQ("آریل")).Only(ctx)
    if err != nil {
        log.Fatalf("فشل در پرس و جوی کاربر: %v", err)
        return err
    }

    // گذر از تمامی ماشین های متعلق به آریل
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("فشل در پرس و جوی ماشین ها: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("آریل یک ماشین با مدل دارد: %s", car.Model)
    }

    // گذر از تمامی گروه هایی که آریل عضو آن هاست
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("فشل در پرس و جوی گروه ها: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("آریل عضو گروه است: %s", g.Name)
    }

    return nil
}

کد فوق یک مثال ابتدایی از گذر از گراف است، که ابتدا یک کاربر را پرس و جو می‌کند و سپس ماشین ها و گروه های کاربر را می‌گذراند.

7. تصویرسازی طرح پایگاه داده

7.1. نصب ابزار اتلس

برای تصویرسازی طرح پایگاه داده تولید شده توسط ent، می‌توان از ابزار اتلس استفاده کرد. مراحل نصب اتلس بسیار ساده است. به عنوان مثال، در macOS می‌توانید از طریق brew آنرا نصب کنید:

brew install ariga/tap/atlas

توجه: اتلس یک ابزار جامع مهاجرت پایگاه داده است که می‌تواند مدیریت نسخه ساختار جدول را برای انواع مختلف پایگاه‌های داده را انجام دهد. معرفی دقیق اتلس در فصل‌های بعدی ارائه می‌شود.

7.2. تولید ERD و طرح SQL

استفاده از اتلس برای مشاهده و خروجی گرفتن از طرح ها بسیار ساده است. بعد از نصب اتلس، می‌توانید از دستور زیر برای مشاهده نمودار رابطه موجودیت (ERD) استفاده کنید:

atlas schema inspect -d [database_dsn] --format dot

یا به صورت مستقیم طرح SQL را تولید کنید:

atlas schema inspect -d [database_dsn] --format sql

که [database_dsn] به نام منبع داده (DSN) پایگاه داده شما اشاره دارد. به عنوان مثال، برای پایگاه داده SQLite، ممکن است این طور باشد:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

خروجی تولید شده توسط این دستورات می‌تواند به وسیله ابزارهای مربوطه به نماها یا اسناد تبدیل شود.

8. مهاجرت طرح

8.1. مهاجرت اتوماتیک و مهاجرت نسخه‌بندی شده

ent از دو راهبرد مهاجرت طرح پشتیبانی می‌کند: مهاجرت اتوماتیک و مهاجرت نسخه‌بندی شده. مهاجرت اتوماتیک فرایند بررسی و اعمال تغییرات طرح در زمان اجراست که برای توسعه و آزمایش مناسب است. مهاجرت نسخه‌بندی شده شامل تولید اسکریپت‌های مهاجرت است و نیازمند بررسی دقیق و آزمایش قبل از استقرار در محیط تولید است.

نکته: برای مهاجرت اتوماتیک، به محتوای بخش 4.1 مراجعه کنید.

8.2. انجام مهاجرت نسخه‌بندی شده

فرایند مهاجرت نسخه‌بندی شده شامل تولید فایل‌های مهاجرت از طریق اتلس است. دستورات مربوط به این کار به شرح زیر است:

برای تولید فایل‌های مهاجرت:

atlas migrate diff -d ent/schema/path --dir migrations/dir

سپس این فایل‌های مهاجرت می‌توانند به پایگاه داده اعمال شوند:

atlas migrate apply -d migrations/dir --url database_dsn

با پیروی از این فرایند، می‌توانید تاریخچه مهاجرت های پایگاه داده را در سیستم کنترل نسخه نگهداری کنید و پیش از هر مهاجرت، بررسی دقیقی انجام دهید.

نکته: به کد نمونه در https://github.com/ent/ent/tree/master/examples/start مراجعه کنید.