1. ent'e Giriş

Ent, özellikle Go diline yönelik olarak Facebook tarafından geliştirilen bir varlık çerçevesidir. Büyük ölçekli veri modeli uygulamaları oluşturmayı ve sürdürmeyi basitleştirir. ent çerçevesi genellikle aşağıdaki prensipleri izler:

  • Veritabanı şemasını grafik yapısı olarak kolayca modelleme.
  • Şemayı Go dilinde kod olarak tanımlama.
  • Kod üretimi temelli statik tipleri uygulama.
  • Veritabanı sorgularını ve grafik dolaşımını yazmak çok kolay.
  • Go şablonları kullanarak kolayca genişletilebilir ve özelleştirilebilir.

2. Ortam Kurulumu

Ent çerçevesini kullanmaya başlamak için geliştirme ortamınızda Go dilinin yüklü olduğundan emin olun.

Eğer proje dizininiz GOPATH'ın dışındaysa veya GOPATH hakkında bilgi sahibi değilseniz, aşağıdaki komutu kullanarak yeni bir Go modül projesi oluşturabilirsiniz:

go mod init entdemo

Bu, yeni bir Go modülü başlatacak ve entdemo projeniz için yeni bir go.mod dosyası oluşturacaktır.

3. İlk Şemanın Tanımlanması

3.1. ent CLI Kullanarak Şema Oluşturma

İlk olarak, kök dizininizde ent CLI aracını kullanarak User adında bir şema oluşturmak için aşağıdaki komutu çalıştırmanız gerekmektedir:

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

Yukarıdaki komut, entdemo/ent/schema/ dizininde User şemasını oluşturacaktır.

Dosya 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. Alan Eklemek

Daha sonra, User Şemasına alan tanımlamaları eklememiz gerekmektedir. Aşağıda, User varlığına iki alan eklemenin bir örneği bulunmaktadır.

Değiştirilmiş dosya entdemo/ent/schema/user.go:

package schema

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

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

Bu kod parçası, User modeli için iki alanı tanımlar: age ve name. Burada, age pozitif bir tamsayı iken name varsayılan olarak "unknown" olan bir dizedir.

3.3. Veritabanı Varlıklarının Oluşturulması

Şemayı tanımladıktan sonra, temel veritabanı erişim mantığını oluşturmak için go generate komutunu çalıştırmanız gerekmektedir.

Projedeki kök dizininde aşağıdaki komutu çalıştırın:

go generate ./ent

Bu komut, önceden tanımlanan şemaya dayalı olarak ilgili Go kodunu oluşturacak ve aşağıdaki dosya yapısını ortaya çıkaracaktır:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (kısalık için birkaç dosya atlandı)
├── 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. Veritabanı Bağlantısını Başlatma

MySQL veritabanına bağlantı kurmak için, ent çerçevesi tarafından sağlanan Open işlevini kullanabiliriz. İlk olarak MySQL sürücüsünü içe aktarın ve ardından doğru bağlantı dizesini sağlayarak veritabanı bağlantısını başlatın.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // MySQL sürücüsünü içe aktar
)

func main() {
    // MySQL veritabanı ile bağlantı kurmak için ent.Open kullanın.
    // Aşağıdaki yer tutucularını "kullanıcı_adınız", "şifreniz" ve "veritabanınız" ile değiştirmeyi unutmayın.
    client, err := ent.Open("mysql", "kullanıcı_adınız:şifreniz@tcp(localhost:3306)/veritabanınız?parseTime=True")
    if err != nil {
        log.Fatalf("mysql'e bağlantı açma başarısız oldu: %v", err)
    }
    defer client.Close()

    // Otomatik göç (migration) aracını çalıştır
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("şema kaynakları oluşturma başarısız oldu: %v", err)
    }
    
    // Ek iş mantığı burada yazılabilir
}

4.2. Varlıklar Oluşturma

Kullanıcı varlığı oluşturmak, yeni bir varlık nesnesi oluşturmak ve onu veritabanına Save veya SaveX yöntemi kullanarak kalıcı hale getirmekle ilgilidir. Aşağıdaki kod, yeni bir Kullanıcı varlığı oluşturmayı ve age ve name alanlarını başlatmayı göstermektedir.

// CreateUser işlevi, yeni bir Kullanıcı varlığı oluşturmak için kullanılır
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Kullanıcı oluşturmak için client.User.Create() kullanın,
    // sonra SetAge ve SetName yöntemlerini zincirleyerek varlık alanlarının değerlerini ayarlayın.
    u, err := client.User.
        Create().
        SetAge(30).    // Kullanıcı yaşını ayarla
        SetName("a8m"). // Kullanıcı adını ayarla
        Save(ctx)     // Varlığı veritabanına kaydetmek için Save'i çağırın
    if err != nil {
        return nil, fmt.Errorf("kullanıcı oluşturma başarısız oldu: %w", err)
    }
    log.Println("kullanıcı oluşturuldu: ", u)
    return u, nil
}

main işlevinde, yeni bir kullanıcı varlığı oluşturmak için CreateUser işlevini çağırabilirsiniz.

func main() {
    // ...Atlanmış veritabanı bağlantısı kurma kodu

    // Bir kullanıcı varlığı oluştur
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("kullanıcı oluşturma başarısız oldu: %v", err)
    }
    log.Printf("oluşturulan kullanıcı: %#v\n", u)
}

4.3. Varlıkları Sorgulama

Varlıkları sorgulamak için, ent tarafından oluşturulan sorgu oluşturucuyu kullanabiliriz. Aşağıdaki kod, "a8m" adında bir kullanıcıyı sorgulamanın nasıl yapıldığını göstermektedir:

// QueryUser işlevi, belirli bir ada sahip kullanıcı varlığını sorgulamak için kullanılır
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Kullanıcı için sorgu oluşturmak için client.User.Query() kullanın,
    // sonra Where yöntemini zincirleyerek sorgu koşullarını ekleyin, örneğin kullanıcı adına göre sorgulama yapma
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // Sorgu koşulunu ekleyin, bu durumda ad "a8m"
        Only(ctx)                      // Only yöntemi yalnızca bir sonuç beklediğini gösterir
    if err != nil {
        return nil, fmt.Errorf("kullanıcı sorgulama başarısız oldu: %w", err)
    }
    log.Println("getirilen kullanıcı: ", u)
    return u, nil
}

main işlevinde, kullanıcı varlığını sorgulamak için QueryUser işlevini çağırabilirsiniz.

func main() {
    // ...Atlanmış veritabanı bağlantısı kurma ve kullanıcı oluşturma kodu

    // Kullanıcı varlığını sorgula
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("kullanıcı sorgulama başarısız oldu: %v", err)
    }
    log.Printf("sorgulanan kullanıcı: %#v\n", u)
}

5.1. Kenarları ve Ters Kenarları Anlama

ent çerçevesinde, veri modeli varlık yapısı olarak görselleştirilir, burada varlıklar grafikteki düğümleri temsil eder ve varlıklar arasındaki ilişkiler kenarlarla gösterilir. Bir kenar, bir varlıktan diğerine bir bağlantıdır, örneğin, bir Kullanıcı birden çok `Araba'ya sahip olabilir.

Ters Kenarlar, varlıklar arasındaki ters referansları temsil ederek, mantıksal olarak varlıklar arasındaki ters ilişkiyi temsil eder, ancak veritabanında yeni bir ilişki oluşturmaz. Örneğin, bir Arabanın ters kenarı aracılığıyla bu arabaya sahip olan Kullanıcıyı bulabiliriz.

Kenarların ve ters kenarların temel önemi, ilişkili varlıklar arasında gezinmeyi çok sezgisel ve doğrudan hale getirmesidir.

İpucu: entte, kenarlar geleneksel veritabanı dış anahtarlarına karşılık gelir ve tablolar arasındaki ilişkileri tanımlamak için kullanılır.

5.2. Şemada Kenarları Tanımlama

İlk olarak, ent CLI'sını kullanarak Car ve Group için başlangıç şemasını oluşturacağız:

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

Sonraki adımda, User şemasında, kullanıcıların arabalarıyla olan ilişkiyi temsil etmek için Car ile kenarı tanımlarız. Kullanıcı varlığındaki Car türüne işaret eden cars adında bir kenar ekleyebiliriz, bu da bir kullanıcının birden çok arabaya sahip olabileceğini gösterir:

// entdemo/ent/schema/user.go

// Kullanıcının kenarları.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

Kenarları tanımladıktan sonra, ilgili kodu oluşturmak için tekrar go generate ./ent komutunu çalıştırmamız gerekmektedir.

5.3. Kenar Verileri Üzerinde İşlem Yapma

Bir kullanıcıyla ilişkili arabaları oluşturmak basit bir süreçtir. Bir kullanıcı varlığı verildiğinde, yeni bir araba varlığı oluşturabilir ve kullanıcıyla ilişkilendirebiliriz:

import (
    "context"
    "log"
    "entdemo/ent"
    // Araba için şema tanımını içe aktardığımızdan emin olun
    _ "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("kullanıcı alınamadı: %v", err)
        return err
    }

    // Yeni bir araba oluştur ve kullanıcıyla ilişkilendir
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("kullanıcı için araba oluşturma başarısız: %v", err)
        return err
    }

    log.Println("araba oluşturuldu ve kullanıcıyla ilişkilendirildi")
    return nil
}

Benzer şekilde, bir kullanıcının arabalarını sorgulamak da basittir. Bir kullanıcının sahip olduğu tüm arabaların listesini almak istiyorsak, aşağıdakileri yapabiliriz:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("kullanıcı alınamadı: %v", err)
        return err
    }

    // Kullanıcının sahip olduğu tüm arabaları sorgula
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("arabaları sorgulama başarısız: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("araba: %v, model: %v", car.ID, car.Model)
    }
    return nil
}

Yukarıdaki adımlarla, hem şemada kenarları nasıl tanımlayacağımızı öğrendik hem de kenarlara ilişkin verileri nasıl oluşturup sorgulayacağımızı gösterdik.

6. Grafik Gezginliği ve Sorgulama

6.1. Grafik Yapılarını Anlama

entte, grafik yapıları varlıklar ve aralarındaki kenarlardan temsil edilir. Her bir varlık grafikte bir düğüme eşdeğerdir ve varlıklar arasındaki ilişkiler kenarlarla temsil edilir, bu ilişkiler bir-birine, bir-çoka, çok-çoka vb. olabilir. Bu grafik yapısı karmaşık sorguları ve ilişkisel veritabanı üzerinde işlemleri basit ve sezgisel hale getirir.

6.2. Grafik Yapıları Üzerinde Gezinme

Grafik Gezinme kodu yazmak genellikle varlıklar arasındaki kenarlar aracılığıyla veri sorgulama ve ilişkilendirme işlemi içerir. Aşağıda, ent içindeki grafik yapısını nasıl gezineceğimizi gösteren basit bir örnek bulunmaktadır:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal, grafik yapısını gezmenin bir örneğidir
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // "Ariel" adlı kullanıcıyı sorgula
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("Kullanıcı sorgulanamadı: %v", err)
        return err
    }

    // Ariel'e ait tüm arabaları gez
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("Arabalar sorgulanamadı: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Ariel'in %s modelinde bir arabası var", car.Model)
    }

    // Ariel'in üye olduğu tüm grupları gez
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("Gruplar sorgulanamadı: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Ariel, %s grubunun üyesi", g.Name)
    }

    return nil
}

Yukarıdaki kod, grafik gezinmenin temel bir örneğidir. İlk olarak bir kullanıcı sorgulanır ve ardından kullanıcının arabaları ve grupları gezilir.

7. Veritabanı Şemasını Görselleştirme

7.1. Atlas Aracını Yükleme

ent tarafından oluşturulan veritabanı şemasını görselleştirmek için Atlas aracını kullanabiliriz. Atlas'ın kurulum adımları çok basittir. Örneğin, macOS üzerinde brew kullanarak şu şekilde yükleyebilirsiniz:

brew install ariga/tap/atlas

Not: Atlas, çeşitli veritabanları için tablo yapı versiyon yönetimini yönetebilen evrensel bir veritabanı göç aracıdır. Atlas'a yönelik detaylı tanıtım ileriki bölümlerde sağlanacaktır.

7.2. ER Diyagramı ve SQL Şeması Oluşturma

Atlas'ı kullanarak şemaları görüntülemek ve dışa aktarmak oldukça basittir. Atlas'ı yükledikten sonra, aşağıdaki komutları kullanabilirsiniz:

Entitiy-Relationship Diyagramını (ERD) görüntülemek için:

atlas schema inspect -d [veritabanı_dsn] --format dot

Veya doğrudan SQL Şeması oluşturabilirsiniz:

atlas schema inspect -d [veritabanı_dsn] --format sql

Burada [veritabanı_dsn], veritabanınızın veri kaynağı adını (DSN) belirtir. Örneğin, bir SQLite veritabanı için şöyle olabilir:

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

Bu komutlarla oluşturulan çıktı, ilgili araçlar kullanılarak görünümlere veya belgelere dönüştürülebilir.

8. Şema Göçü

8.1. Otomatik ve Versiyonlu Göç

ent, otomatik göç ve versiyonlu göç olmak üzere iki şema göçü stratejisini destekler. Otomatik göç, çalışma zamanında şema değişikliklerini inceleyip uygulama sürecidir; geliştirme ve test için uygundur. Versiyonlu göç ise göç betikleri oluşturmayı içerir ve üretim dağıtımından önce dikkatli bir inceleme ve test gerektirir.

İpucu: Otomatik göç için bölüm 4.1'deki içeriğe bakınız.

8.2. Versiyonlu Göç Yapma

Versiyonlu göç süreci, Atlas aracılığıyla göç dosyaları oluşturmayı içerir. İlgili komutlar aşağıdaki gibidir:

Göç dosyalarını oluşturmak için:

atlas migrate diff -d ent/schema/yol --dir göçler/dizini

Daha sonra bu göç dosyaları veritabanına uygulanabilir:

atlas migrate apply -d göçler/dizini --url veritabanı_dsn

Bu sürecin ardından veritabanı göçlerinin her birinden önce kapsamlı inceleme yapılacak ve versiyon kontrol sisteminde bir göç geçmişi tutulacaktır.

İpucu: Örnek kod için https://github.com/ent/ent/tree/master/examples/start adresine bakınız.