1. Varlık ve İlişki Temel Kavramları

ent çerçevesinde, bir varlık veritabanında yönetilen temel veri birimidir ve genellikle veritabanındaki bir tabloya karşılık gelir. Varlıkta bulunan alanlar, tablodaki sütunlara karşılık gelir, varlık (kenarlar) arasındaki ilişkiler ise varlıklar arasındaki ilişkileri ve bağımlılıkları tanımlamak için kullanılır. Varlık ilişkileri, karmaşık veri modelleri oluşturmanın temelini oluşturur ve ebeveyn-çocuk ilişkileri ve sahiplik ilişkileri gibi hiyerarşik ilişkilerin temsil edilmesine olanak tanır.

ent çerçevesi, varlık şemasındaki bu ilişkileri tanımlamak ve yönetmek için zengin bir dizi API sağlar. Bu ilişkiler aracılığıyla, veri arasındaki karmaşık iş mantığını kolayca ifade edebilir ve işlemler gerçekleştirebiliriz.

2. ent Çerçevesinde Varlık İlişkilerinin Türleri

2.1 Bir-Bir (O2O) İlişki

Bir-bir ilişkisi, iki varlık arasındaki bir-bir karşılıklı ilişkiyi ifade eder. Örneğin, kullanıcılar ve banka hesapları durumunda, her kullanıcının sadece bir banka hesabı olabilir ve her banka hesabı da yalnızca bir kullanıcıya ait olabilir. ent çerçevesi, bu tür ilişkileri tanımlamak için edge.To ve edge.From yöntemlerini kullanır.

Öncelikle, User şeması içinde Card'a işaret eden bir bir-bir ilişkisi tanımlayabiliriz:

// User'ın kenarları.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Card varlığına işaret eder, ilişki adını "card" olarak tanımlar
            Unique(),               // Unique yöntemi, bu ilişkinin bir-bir ilişkisi olduğunu sağlar
    }
}

Daha sonra, Card şeması içinde User'a ters ilişkiyi tanımlarız:

// Card'ın kenarları.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Karttan kullanıcıya geri işaret eder, ilişki adını "owner" olarak tanımlar
            Ref("card").              // Ref yöntemi, karşılıklı ters ilişki adını belirtir
            Unique(),                 // Bir kartın bir sahibe karşılık gelmesini sağlamak için benzersiz işaretler
    }
}

2.2 Bir-Çok (O2M) İlişki

Bir-çok ilişkisi, bir varlığın birden fazla diğer varlıkla ilişkilendirilebileceğini, ancak bu varlıkların yalnızca tek bir varlığa işaret edebileceğini gösterir. Örneğin, bir kullanıcının birden fazla evcil hayvanı olabilir, ancak her evcil hayvanın yalnızca bir sahibi olabilir.

ent içinde, bu tür ilişkiyi tanımlamak için hala edge.To ve edge.From kullanırız. Aşağıdaki örnek, kullanıcılar ve evcil hayvanlar arasında bir-birçok ilişkiyi tanımlar:

// User'ın kenarları.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // User varlığından Pet varlığına bir-çok ilişki
    }
}

Pet varlığında, User'a geri bir-çok ilişki tanımlarız:

// Pet'in kenarları.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Pet'ten User'a bir-çok ilişki
            Ref("pets").              // Evcil hayvandan sahibe geri ilişki adını belirtir
            Unique(),                 // Bir sahibin birden fazla evcil hayvana sahip olmasını sağlar
    }
}

2.3 Çok-Çok (M2M) İlişki

Çok-çok ilişkisi, iki tür varlığın birbirlerine birden fazla örneğe sahip olmalarını sağlar. Örneğin, bir öğrenci birden fazla derse kaydolabilir ve bir ders de birden fazla öğrenciye sahip olabilir. ent bu tür ilişkileri kurmak için bir API sağlar:

Student varlığında, Course ile çok-çok ilişkiyi edge.To kullanarak tanımlarız:

// Student'ın kenarları.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Student'tan Course'a çok-çok ilişki tanımlar
    }
}

Benzer şekilde, Course varlığında, çok-çok ilişkinin ters ilişkisini Studenta tanımlarız:

// Course'ın kenarları.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Course'tan Student'a çok-çok ilişki tanımlar
            Ref("courses"),                  // Dersden Öğrenciye ters ilişki adını belirtir
    }
}

Bu tür ilişkiler, karmaşık uygulama veri modelleri oluşturmanın temelidir ve bunları ent içinde nasıl tanımlayıp kullanacağımızı anlamak, veri modellerini ve iş mantığını genişletmek için oldukça önemlidir.

3. Varlık İlişkileri için Temel İşlemler

Bu bölüm, belirlenmiş ilişkilerle ent kullanarak temel işlemleri gerçekleştirmeyi, dahil olmak üzere varlık oluşturma, sorgulama ve ilişkili varlıkları gezme işlemlerini gösterecektir.

3.1 İlişkili Varlıkların Oluşturulması

Varlıkları oluştururken, varlıklar arasındaki ilişkileri aynı anda ayarlayabilirsiniz. Bir birçok için (O2M) ve birçok için (M2M) ilişkiler için Add{Edge} yöntemini kullanarak ilişkili varlıkları ekleyebilirsiniz.

Örneğin, bir kullanıcı varlığı ve bir evcil hayvan varlığı belirli bir ilişkiyle, bir kullanıcının birden fazla evcil hayvana sahip olabileceği durumu düşünelim. Aşağıdaki, yeni bir kullanıcı oluşturup onun için evcil hayvanlar eklemek örneğidir:

// Kullanıcı oluşturup evcil hayvanlar ekler
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Evcil hayvan örneği oluştur
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Kullanıcı örneği oluştur ve evcil hayvanla ilişkilendir
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Evcil hayvanı ilişkilendirmek için AddPets yöntemini kullan
        SaveX(ctx)

    return user, nil
}

Bu örnekte, önce Fido adında bir evcil hayvan örneği oluşturuyoruz, daha sonra Alice adında bir kullanıcı oluşturuyoruz ve AddPets yöntemini kullanarak evcil hayvan örneğini kullanıcıyla ilişkilendiriyoruz.

3.2 İlişkili Varlıkların Sorgulanması

İlişkili varlıkların sorgulanması, entte yaygın bir işlemdir. Örneğin, belirli bir varlıkla ilişkili diğer varlıkları almak için Query{Edge} yöntemini kullanabilirsiniz.

Kullanıcılar ve evcil hayvanlar örneğimizle devam edecek olursak, bir kullanıcının sahip olduğu tüm evcil hayvanları sorgulamanın bir örneği şöyle olabilir:

// Bir kullanıcının tüm evcil hayvanlarını sorgula
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Kullanıcı kimliğine göre kullanıcı örneğini al
        QueryPets().      // Kullanıcıyla ilişkili evcil hayvan varlıklarını sorgula
        All(ctx)          // Sorgulanan tüm evcil hayvan varlıklarını döndür
    if err != nil {
        return nil, err
    }

    return pets, nil
}

Yukarıdaki kod örneğinde, önce kullanıcı kimliğine göre kullanıcı örneğini alıyoruz, ardından QueryPets yöntemini çağırarak bu kullanıcıyla ilişkili tüm evcil hayvan varlıklarını alıyoruz.

Not: ent'in kod üretim aracı, tanımlı varlık ilişkilerine dayalı olarak ilişki sorguları için API'yi otomatik olarak oluşturur. Üretilen kodları incelemeniz önerilir.

4. Önceden Yükleme (Eager Loading)

4.1 Önceden Yükleme İlkeleri

Önceden yükleme, veritabanı sorgularında ilişkili varlıkların önceden alınması ve yüklenmesi için kullanılan bir tekniktir. Bu yaklaşım, birden çok varlıkla ilgili verileri tek seferde almak için yaygın olarak kullanılır, böylece ardışık işlemlerde birden çok veritabanı sorgusu işlemi yapmaktan kaçınılarak uygulamanın performansını önemli ölçüde arttırır.

Ent çatısında, önceden yükleme öncelikle birçoklu ve birçoklu gibi varlık arasındaki ilişkileri işlemek için kullanılır. Veritabanından bir varlık alındığında, ilişkili varlıklar otomatik olarak yüklenmez. Bunun yerine, ihtiyaç duyulduğunda önceden yükleme ile açıkça yüklenirler. Bu, ayrı ayrı sorgular için N+1 sorgu problemine (her ana varlık için ayrı ayrı sorgular gerçekleştirme) bir çözüm olarak önemlidir.

Ent çatısında, önceden yükleme, sorgu oluşturucuda With yöntemi kullanılarak gerçekleştirilir. Bu yöntem, her kenar için WithGroups ve WithPets gibi ilgili With... işlevlerini oluşturur. Bu yöntemler, ent çatısı tarafından otomatik olarak oluşturulur ve programcılar belirli ilişkilerin önceden yüklenmesini istemek için bunları kullanabilirler.

Varlıkların önceden yüklenmesi prensibinin çalışma prensibi, birincil varlık sorgulandığında, ent tarafından tüm ilişkili varlıkları almak için ek sorgular yürütülmesidir. Daha sonra, bu varlıklar dönen nesnenin Edges alanına doldurulur. Bu, en azından önceden yüklenmesi gereken her ilişkili kenar için en az bir kez olmak üzere, birden çok veritabanı sorgusu yürütmesi anlamına gelir. Bu yöntem bazı senaryolarda tek bir karmaşık JOIN sorgusundan daha az verimli olabilir, ancak daha büyük esneklik sunar ve ent'in gelecekteki sürümlerinde performans optimizasyonları alacağı öngörülmektedir.

4.2 Önceden Yükleme Uygulaması

Şimdi, ent çatısı içinde önceden yükleme işlemlerinin modelleri kullanarak bazı örnek kodlar üzerinden nasıl gerçekleştirildiğini gösterelim.

Tekli İlişki Ön Yükleme

Diyelim ki veritabanından tüm kullanıcıları ve evcil hayvan verilerini ön yüklemek istiyoruz. Bunun için aşağıdaki kodu yazarak bu işlemi gerçekleştirebiliriz:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Hata yönetimi
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("Kullanıcı (%v), evcil hayvan sahiplendi (%v)\n", u.ID, p.ID)
    }
}

Bu örnekte, kullanıcılarla ilişkili evcil hayvan varlıklarını ön yüklemek için WithPets yöntemini kullanıyoruz. Ön yüklenen evcil hayvan verileri, ilişkili verilere erişebileceğimiz Edges.Pets alanına yerleştirilir.

Çoklu İlişki Ön Yükleme

ent bize bir seferde birden fazla ilişki ön yüklemesini yapma imkanı sağlar, hatta önceden filtreleme, sıralama, sınırlama yapılandırması yapmak veya iç içe geçmiş ilişkileri ön yüklemek mümkündür. Aşağıdaki örnek, yöneticilerin evcil hayvanlarını ve ait oldukları takımları ön yüklerken aynı zamanda takımlarla ilişkili kullanıcıları da ön yüklemektedir:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // İlk 5 takımla sınırla
        q.Order(ent.Asc(group.FieldName)) // Takım adına göre artan sırayla sırala
        q.WithUsers()       // Takımdaki kullanıcıları ön yükle
    }).
    All(ctx)
if err != nil {
    // Hataları yönet
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("Yönetici (%v), evcil hayvan sahiplendi (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("Yönetici (%v), takım (%v)'e ait\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("Takım (%v), üye (%v)'e sahip\n", g.ID, u.ID)
        }
    }
}

Bu örnek üzerinden ent'in ne kadar güçlü ve esnek olduğunu görebilirsiniz. Sadece birkaç basit yöntem çağrısı ile zengin ilişkili verileri ön yükleyebilir ve yapılandırabilir. Bu, veri odaklı uygulamalar geliştirmek için büyük bir kolaylık sağlar.