1. مکانیزم هوک‌ها

مکانیزم هوک‌ها یک روش برای افزودن منطق سفارشی قبل یا بعد از تغییرات خاص در عملیات پایگاه داده است. زمانی که اسکیمای پایگاه داده را تغییر می‌دهیم، مانند اضافه کردن گره‌های جدید، حذف یال‌ها بین گره‌ها یا حذف چند گره، می‌توانیم از هوک‌ها برای انجام اعتبارسنجی داده، ثبت وقایع، بررسی مجوزها یا هر عملیات سفارشی دیگر استفاده کنیم. این برای اطمینان از همسانی داده و اطلاعات با قوانین تجاری بسیار حیاتی است و به توسعه دهندگان این امکان را می‌دهد که از امکانات اضافی بدون تغییر منطق تجاری اصلی استفاده کنند.

2. روش ثبت هوک‌ها

2.1 هوک‌های سراسری و هوک‌های محلی

هوک‌های سراسری (هوک‌های زمان اجرا) برای همه انواع عملیات در گراف مؤثر هستند. آن‌ها برای افزودن منطق به کل برنامه مناسب هستند، مانند ثبت و نظارت. هوک‌های محلی (هوک‌های اسکیما) در داخل اسکیمای انواع خاص تعریف می‌شوند و فقط برای عملیات تغییری که با نوع اسکیما همخوانی دارد، اعمال می‌شوند. استفاده از هوک‌های محلی این امکان را فراهم می‌کند که تمام منطق مربوط به انواع خاص گره‌ها را در یک مکان، یعنی تعریف اسکیما، متمرکز کنیم.

2.2 مراحل ثبت هوک‌ها

ثبت یک هوک در کد معمولاً شامل مراحل زیر است:

  1. تعریف تابع هوک. این تابع یک ent.Mutator را می‌گیرد و یک ent.Mutator را برمی‌گرداند. به عنوان مثال، ایجاد یک هوک ساده برای ثبت وقایع:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // قبل از عملیات متغیر
        log.Printf("قبل از متغیرسازی: نوع=%s، عملیات=%s\n", m.Type(), m.Op())
        // انجام عملیات متغیرسازی
        v, err := next.Mutate(ctx, m)
        // بعد از عملیات متغیرسازی
        log.Printf("بعد از متغیرسازی: نوع=%s، عملیات=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. ثبت هوک با کلاینت. برای هوک‌های سراسری، می‌توانید آن‌ها را با استفاده از روش Use کلاینت ثبت کنید. برای هوک‌های محلی، می‌توانید آن‌ها را در اسکیما با استفاده از روش Hooks نوع ثبت کنید.
// ثبت یک هوک سراسری
client.Use(logHook)

// ثبت یک هوک محلی، فقط برای نوع کاربر
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // اضافه کردن منطق خاص
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. می‌توانید چند هوک را به زنجیره وصل کنید و آن‌ها به ترتیب ثبت اجرا خواهند شد.

3. ترتیب اجرای هوک‌ها

ترتیب اجرای هوک‌ها توسط ترتیب ثبت آن‌ها با کلاینت تعیین می‌شود. به عنوان مثال، client.Use(f, g, h) در عملیات متغیرسازی را به ترتیب f(g(h(...))) اجرا خواهد کرد. در این مثال، f اولین هوکی است که اجرا می‌شود، سپس g و در نهایت h.

مهم است که هوک‌های زمان اجرا اولویت بیشتری نسبت به هوک‌های اسکیما داشته باشند. این بدان معنی است که اگر g و h هوک‌های تعریف شده در اسکیما باشند در حالی که f با استفاده از client.Use(...) ثبت شده باشد، ترتیب اجرا f(g(h(...))) خواهد بود. این باعث می‌شود که منطق سراسری، مانند ثبت وقایع، قبل از همه هوک‌های دیگر اجرا شود.

4. برخورد با مشکلات ناشی از هوک‌ها

زمانی که با استفاده از هوک‌ها عملیات پایگاه داده را سفارشی کرده‌ایم، ممکن است به مشکلاتی نظیر حلقه وار وابستگی برخورد کنیم. این معمولاً زمانی رخ می‌دهد که سعی می‌کنیم از هوک‌های اسکیما استفاده کنیم زیرا بسته ent/schema ممکن است بسته هسته ent را معرفی کند. اگر بسته هسته ent هم سعی کند که بسته ent/schema را وارد کند، یک وابستگی دایره‌ای شکل می‌گیرد.

عوامل ایجاد وابستگی دایره‌ای

وابستگی‌های دایره‌ای معمولاً از وابستگی‌های دوطرفه بین تعاریف اسکیما و کد نهایت تولید شده توسط موجودات تولید شده می‌آیند. این بدان معنی است که ent/schema به ent وابسته است (زیرا نیاز دارد تا از انواع ارائه شده توسط چارچوب ent استفاده کند) در حالی که کد تولید شده توسط ent هم به ent/schema وابسته است (زیرا نیاز دارد تا به اطلاعات اسکیمای در آن تعریف‌شده دسترسی داشته باشد).

رفع مشکل وابستگی‌های چرخشی

اگر با خطای وابستگی‌های چرخشی رو به رو شدید، می‌توانید از مراحل زیر پیروی کنید:

  1. ابتدا تمام هوک‌های استفاده شده در ent/schema را کامنت کنید.
  2. سپس، انواع سفارشی که در ent/schema تعریف شده‌اند را به یک بسته جدید منتقل کنید. به عنوان مثال، می‌توانید یک بسته به نام ent/schema/schematype ایجاد کنید.
  3. دستور go generate ./... را اجرا کنید تا بسته ent به مسیر بسته جدید اشاره کند و مراجع نوع در طرح را به‌روز کند. به عنوان مثال، schema.T را به schematype.T تغییر دهید.
  4. مراجع هوک‌های قبلاً کامنت شده را آنکامنت کرده و دستور go generate ./... را دوباره اجرا کنید. در این نقطه، تولید کد بدون خطا باید ادامه یابد.

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

5. استفاده از توابع کمکی هوک

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

// تنها HookA را برای عملیات UpdateOne و DeleteOne اجرا کن
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// در عملیات Create، HookB را اجرا نکن
hook.Unless(HookB(), ent.OpCreate)

// HookC را تنها زمانی اجرا کن که Mutation در حال تغییر فیلد "status" و پاکسازی فیلد "dirty" باشد
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// مانع تغییر فیلد "password" در عملیات Update (چندگانه) شو
hook.If(
    hook.FixedError(errors.New("رمز عبور نمی‌تواند در عملیات بروزرسانی چندگانه ویرایش شود")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

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

6. هوک‌های تراکنش

هوک‌های تراکنش اجازه می‌دهند تا هوک‌های خاصی هنگامی که یک تراکنش تایید شود (Tx.Commit) یا برگشت داده شود (Tx.Rollback) اجرا شوند. این بسیار مفید است برای اطمینان از یکپارچگی داده و اتمام عملیات.

مثالی از هوک‌های تراکنش

client.Tx(ctx, func(tx *ent.Tx) error {
    // ثبت یک هوک تراکنش - هوکBeforeCommit پیش از تایید اجرا می‌شود.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // منطق قبل از تایید واقعی را می‌توان در اینجا قرار داد.
            fmt.Println("قبل از تایید")
            return next.Commit(ctx, tx)
        })
    })

    // انجام سری عملیات درون تراکنش...

    return nil
})

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