1. Hooks Mekanizması

Hooks mekanizması, veritabanı işlemlerinde belirli değişikliklerden önce veya sonra özel mantık eklemek için kullanılan bir yöntemdir. Veritabanı şemasını değiştirirken, yeni düğümler eklerken, düğümler arasındaki kenarları silerken veya birden fazla düğümü silerken, Hooks kullanarak veri doğrulama, günlüğe kayıt, izin kontrolü veya özel operasyonlar yapabiliriz. Bu, veri tutarlılığını ve iş kurallarına uyumu sağlamanın yanı sıra geliştiricilere orijinal iş mantığını değiştirmeden ek işlevsellik eklemelerine olanak tanır.

2. Hook Kaydetme Yöntemi

2.1 Global Hooks ve Yerel Hooks

Global hooks (Runtime hooks), grafikteki tüm işlemler için etkilidir. Uygulama genelindeki mantık eklemek için uygun olup, günlükleme ve izleme gibi işlevleri eklemek için uygundur. Yerel hooks (Schema hooks), belirli tür şemaları içinde tanımlanır ve yalnızca şemanın türüyle eşleşen mutasyon işlemlerine uygulanır. Yerel hooks kullanarak, belirli düğüm türleriyle ilgili tüm mantık bir araya getirilerek, yani şema tanımında tek bir yerde merkezileştirilmiş olur.

2.2 Hooks Kaydetme Adımları

Kod içinde bir hook kaydetmek genellikle şu adımları içerir:

  1. Hook fonksiyonunu tanımlamak. Bu işlev bir ent.Mutator alır ve bir ent.Mutator döndürür. Örneğin, basit bir günlükleme hook'u oluşturma:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Mutasyon işleminden önce logları yazdır
        log.Printf("Mutasyon öncesi: Tür=%s, İşlem=%s\n", m.Type(), m.Op())
        // Mutasyon işlemi gerçekleştir
        v, err := next.Mutate(ctx, m)
        // Mutasyon işleminden sonra logları yazdır
        log.Printf("Mutasyon sonrası: Tür=%s, İşlem=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Hook'u istemciye kaydetmek. Global hook'lar için, istemciyi Use yöntemiyle kaydedebilirsiniz. Yerel hook'lar için, türdeki Hooks yöntemini kullanarak şemada kaydedebilirsiniz.
// Global hook kaydetme
client.Use(logHook)

// Yerel hook kaydetme, sadece 'User' türüne uygulanır
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Özel mantık ekle
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. Birden çok hook'u zincirleyebilirsiniz ve kayıt sırasında bunlar sırayla çalıştırılır.

3. Hooks'un Çalışma Sırası

Hook'ların çalışma sırası, bunların istemci ile kaydedildiği sıraya göre belirlenir. Örneğin, client.Use(f, g, h) örneğinde, mutasyon işleminde f(g(h(...))) sırasında çalıştırılır. Bu örnekte, önce f, ardından g ve en sonunda da h çalıştırılır.

Önemli bir nokta olarak runtime hook'lar (Runtime hooks), schema hook'ların (Schema hooks) önüne geçer. Bu, eğer g ve h hook'ları şema içinde tanımlanmışsa, f ise client.Use(...) kullanılarak kaydedilmişse, çalıştırma sırasının f(g(h(...))) olacağı anlamına gelir. Bu, günlükleme gibi genel mantığın, diğer tüm hook'lardan önce çalıştırılmasını sağlar.

4. Hook'ların Neden Olduğu Sorunlarla Başa Çıkma

Hooks'u özelleştirirken, genellikle import döngüsü sorunuyla karşılaşabiliriz. Bu genellikle, ent/schema paketinin ent çekirdek paketini tanıtmasından kaynaklanır. Eğer ent çekirdek paketi de ent/schema'yı almaya çalışıyorsa, döngüsel bir bağımlılık oluşur.

Döngüsel Bağımlılıkların Sebepleri

Döngüsel bağımlılıklar genellikle, şema tanımları ile oluşturulan varlık kodu arasındaki karşılıklı bağımlılıklardan kaynaklanır. Bu, ent/schema'nın ent'e bağımlı olmasından kaynaklanır (çünkü ent çerçevesi tarafından sağlanan türleri kullanması gerekir), ent tarafından oluşturulan kodun da ent/schema'ya bağımlı olmasından kaynaklanır (içinde tanımlanan şema bilgilerine erişmesi gerekir).

Dairesel Bağımlılıkları Çözme

Eğer dairesel bağımlılık hatası ile karşılaşırsan, aşağıdaki adımları izleyebilirsin:

  1. İlk olarak, ent/schema içinde kullanılan tüm kancaları yorum satırına al.
  2. Daha sonra, ent/schema içinde tanımlanan özel tipleri yeni bir pakete taşı, örneğin ent/schema/schematype adında bir paket oluşturabilirsin.
  3. go generate ./... komutunu çalıştırarak ent paketini güncelle, böylece şema içinde tiplerin referanslarını güncelle. Örneğin, schema.T'yi schematype.T'ye değiştir.
  4. Daha önce yorum satırına alınmış olan kancaların referanslarını aç ve tekrar go generate ./... komutunu çalıştır. Bu noktada, kod üretimi hatalar olmadan devam etmelidir.

Bu adımları izleyerek, Kancaların içeriği tarafından kaynaklanan dairesel bağımlılık sorununu çözebilir, şema mantığı ile Kancaların uygulanmasının sorunsuz bir şekilde devam etmesini sağlayabiliriz.

5. Hook Yardımcı Fonksiyonlarının Kullanımı

ent çerçevesi, Kancaların çalışma zamanını kontrol etmemize yardımcı olan bir dizi kancayı destekleyen yardımcı fonksiyonlar sağlar. İşte sıkça kullanılan Hook yardımcı fonksiyonlarından bazı örnekler:

// Sadece UpdateOne ve DeleteOne işlemleri için HookA'yı icra et
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Create işlemi sırasında HookB'nin çalıştırılmasını engelle
hook.Unless(HookB(), ent.OpCreate)

// HookC sadece "status" alanını değiştiren Mutation ve "dirty" alanını temizleyen durumlarda icra et
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Update (çoklu) işlemlerinde "password" alanını değiştirmeyi yasakla
hook.If(
    hook.FixedError(errors.New("password güncellemede düzenlenemez")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Bu yardımcı fonksiyonlar, farklı işlemler için Hook'ların etkinleşme koşullarını tam olarak kontrol etmemize olanak tanır.

6. İşlem Kancaları

İşlem Kancaları, bir işlem yapıldığında (Tx.Commit) veya geri alındığında (Tx.Rollback) belirli Kancaların çalıştırılmasına izin verir. Bu, veri tutarlılığını ve işlemlerin atomikliğini sağlamak için çok kullanışlıdır.

İşlem Kancaları Örneği

client.Tx(ctx, func(tx *ent.Tx) error {
    // Bir işlem kancası kaydetme - hookBeforeCommit, commit'ten önce icra edilecektir.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // Gerçek commit'ten önceki mantığı buraya yerleştirebilirsin.
            fmt.Println("Commit'ten önce")
            return next.Commit(ctx, tx)
        })
    })

    // İşlem içerisinde bir dizi işlem gerçekleştir...

    return nil
})

Yukarıdaki kod, bir işlemde commit'ten önce çalışacak bir işlem kancasını kaydetmenin nasıl yapıldığını göstermektedir. Bu kancalar, tüm veritabanı işlemleri gerçekleştirildikten sonra ve işlem gerçekten commit edilmeden önce çağrılacaktır.