EF Core'da bir kolonun değerini uygulama yerine veritabanının üretmesini üç ana yolla sağlarsınız: HasDefaultValue veya HasDefaultValueSql ile varsayılan değer, HasComputedColumnSql ile hesaplanan kolon (computed column) ve anahtarlar için IDENTITY, sequence ya da GUID üretimi. Entity Framework Core, tek kolonlu sayısal ve GUID birincil anahtarlar için değer üretimini zaten konvansiyonla kurar; diğer property'ler için ValueGeneratedOnAdd ve ValueGeneratedOnAddOrUpdate ile EF'e değerin veritabanında oluşacağını söylersiniz.

Bu yazıda varsayılan değerleri (EF Core 10 ile gelen adlandırılmış default constraint'ler dahil), sanal ve kalıcı hesaplanan kolonları, anahtar üretimini, oluşturma ve güncelleme zaman damgalarını, değer üretimini geçersiz kılmayı ve kapatmayı, SQL Server ve SQLite'a özgü ayrıntıları ele alıyoruz. Anahtarın kendisinin nasıl tanımlandığı primary key yazısında, eşzamanlılık için kullanılan rowversion concurrency yazısında anlatılıyor. Serinin tamamı EF Core öğrenme rehberi sayfasında.

Varsayılan değer: HasDefaultValue ve HasDefaultValueSql

İlişkisel veritabanlarında bir kolona varsayılan değer tanımlanabilir: satır o kolon için değer verilmeden eklenirse varsayılan kullanılır. EF Core'da sabit bir değer için HasDefaultValue, değeri veritabanında hesaplayan bir SQL parçası için HasDefaultValueSql kullanırsınız. Belgelerin hatırlattığı bir ayrıntı: varsayılan değer, tabloya yeni bir kolon eklenirken de etkilidir; henüz değeri olmayan mevcut satırlara varsayılan değer yazılır.

csharp
modelBuilder.Entity<Blog>()
    .Property(b => b.Rating)
    .HasDefaultValue(3);

modelBuilder.Entity<Blog>()
    .Property(b => b.Created)
    .HasDefaultValueSql("getdate()");

HasDefaultValueSql içine yazdığınız metin olduğu gibi veritabanına gider; dolayısıyla sağlayıcıya özgüdür. getdate() SQL Server fonksiyonudur, başka bir veritabanında farklı bir fonksiyon gerekir. Tarih fonksiyonu seçerken de dikkatli olun: belgeler, GETDATE() ile GETUTCDATE() gibi birden çok seçenek bulunabileceğini ve doğru olanı seçmeniz gerektiğini hatırlatır. Value conversion belgeleri de veritabanı erişim kodunun tamamında tutarlı olarak UTC kullanmayı ve yerel saati yalnızca kullanıcıya gösterirken ele almayı düşünmenizi önerir (value conversion yazısı).

EF Core 10: default constraint'lere ad vermek

SQL Server'da varsayılan değerler birer default constraint olarak oluşturulur ve adlarını normalde veritabanı üretir. EF Core 10 ile bu adı kendiniz verebilirsiniz: HasDefaultValue ve HasDefaultValueSql ikinci parametre olarak constraint adını alır. Tüm default constraint'lerin EF tarafından otomatik adlandırılmasını isterseniz model düzeyinde UseNamedDefaultConstraints çağırırsınız.

csharp
modelBuilder.Entity<Blog>()
    .Property(b => b.Rating)
    .HasDefaultValue(3, "DF_Blog_IsActive");

modelBuilder.Entity<Blog>()
    .Property(b => b.Created)
    .HasDefaultValueSql("getdate()", "DF_Blog_IsActive");

// Tum default constraint'leri EF adlandirsin:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.UseNamedDefaultConstraints();
}

Hesaplanan kolonlar: sanal ve kalıcı

Çoğu ilişkisel veritabanında bir kolonun değeri, genellikle diğer kolonlara başvuran bir ifadeyle veritabanında hesaplanabilir. HasComputedColumnSql varsayılan olarak sanal (virtual) bir hesaplanan kolon oluşturur: değer her okunduğunda yeniden hesaplanır ve diskte saklanmaz. stored: true verirseniz kolon kalıcı (stored, bazı veritabanlarında persisted) olur: değer satırın her güncellenmesinde hesaplanır ve normal kolonların yanında diskte saklanır.

csharp
modelBuilder.Entity<Person>()
    .Property(p => p.DisplayName)
    .HasComputedColumnSql("[LastName] + ', ' + [FirstName]");

modelBuilder.Entity<Person>()
    .Property(p => p.NameLength)
    .HasComputedColumnSql("LEN([LastName]) + LEN([FirstName])", stored: true);

İkisi arasında seçim yaparken okuma ve yazma maliyetini tartarsınız: sanal kolon yer kaplamaz ama her okumada hesaplanır; kalıcı kolon yazma sırasında hesaplanıp saklanır. Performans belgeleri kalıcı kolonların bir kullanım alanını daha gösterir: sorgunuz bir kolon üzerindeki ifadeyle filtreleme yapıyorsa (örneğin price / 2) basit bir index kullanılamaz; ifade için kalıcı bir hesaplanan kolon tanımlayıp o kolona index koyabilirsiniz (index yazısı). Hesaplanan kolonlara uygulamadan açık değer yazamazsınız; bu konuya aşağıda değiniyoruz.

Birincil anahtarlarda değer üretimi

Birincil anahtarlarda değer üretimi: Tek kolonlu sayısal PK, Guid PK, Açık değer ekleme, Ayar API'si

Belgelere göre composite olmayan short, int, long ve Guid birincil anahtarlar, uygulama bir değer vermediğinde eklenen kayıtlar için değer üretilecek şekilde konvansiyonla kurulur. Gerekli ayarı genellikle sağlayıcı yapar: SQL Server'da sayısal birincil anahtar bir IDENTITY kolonu olur. SQLite'ta ise tamsayı birincil anahtarlar, composite key'in parçası değillerse ve üzerlerinde foreign key yoksa AUTOINCREMENT ile oluşturulur. Kalıtım hiyerarşilerinde anahtar üretiminin stratejiye göre değişen ayrıntıları için TPH, TPT ve TPC yazısına bakın.

DurumSQL ServerSQLite
Tek kolonlu sayısal PKIDENTITY kolonu (seed 1, artış 1)AUTOINCREMENT (composite değil ve FK yoksa)
Guid PKSağlayıcı istemcide sıralı GUID üretirSağlayıcı belgesinde ayrıca anlatılmaz
Açık değer eklemeVarsayılan olarak başarısız; IDENTITY_INSERT gerekirValueGeneratedNever ile uygulama değer verir
Ayar API'siUseIdentityColumn, HasSequenceUseAutoincrement (EF Core 10)

Anahtar dışı property'lerde açık değer üretimi

Anahtar dışı property'lerde açık değer üretimi

Anahtar olmayan bir property için de EF'e değerin eklemede veya hem eklemede hem güncellemede veritabanında üretildiğini söyleyebilirsiniz. [DatabaseGenerated(DatabaseGeneratedOption.Identity)] veya ValueGeneratedOnAdd() değerin ekleme sırasında, [DatabaseGenerated(DatabaseGeneratedOption.Computed)] veya ValueGeneratedOnAddOrUpdate() ise ekleme ve güncelleme sırasında üretildiğini belirtir.

csharp
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public DateTime Inserted { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime LastUpdated { get; set; }
}

// Fluent API karsiligi
modelBuilder.Entity<Blog>()
    .Property(b => b.Inserted)
    .ValueGeneratedOnAdd();

modelBuilder.Entity<Blog>()
    .Property(b => b.LastUpdated)
    .ValueGeneratedOnAddOrUpdate();

Burada çok sık yanlış anlaşılan nokta şudur: bu ayarlar değerin nasıl üretileceğini söylemez. Varsayılan değer ve hesaplanan kolonun aksine yalnızca "bu değer veritabanından gelecek" bilgisini verir; üretimin nasıl yapılacağı sağlayıcıya bağlıdır. Sağlayıcı bazı tipler için üretimi kendisi kurar, bazıları için kurmaz. Belgelerdeki örnekler: SQL Server'da GUID birincil anahtar için sağlayıcı istemci tarafında sıralı GUID üretir; ama bir DateTime property'sine ValueGeneratedOnAdd vermenin hiçbir etkisi yoktur. Benzer şekilde eklemede ve güncellemede üretilen olarak işaretlenmiş ve concurrency token olan byte[] property'leri rowversion tipine eşlenir, ama yalnızca ValueGeneratedOnAdd vermenin etkisi yoktur.

Oluşturma ve güncelleme zaman damgaları

Sık istenen iki kolon, satırın ilk eklendiği an ile son güncellendiği andır. Bunları yapmanın birden çok yolu olduğu için EF Core sağlayıcıları tarih/saat kolonları için değer üretimini genellikle kendiliğinden kurmaz; sizin yapılandırmanız gerekir.

Oluşturma zamanı için çoğu zaman uygun SQL fonksiyonunu varsayılan değer yapmak yeterlidir: SQL Server'da HasDefaultValueSql("getdate()"). Güncelleme zamanı daha zordur. Kalıcı hesaplanan kolon iyi bir çözüm gibi görünse de veritabanları genellikle GETDATE() gibi fonksiyonların hesaplanan kolonda kullanılmasına izin vermez. Belgelerin önerdiği alternatif bir veritabanı trigger'ıdır. Aşağıdaki SQL Server trigger'ı belgelerden alınmıştır:

sql
CREATE TRIGGER [dbo].[Blogs_UPDATE] ON [dbo].[Blogs]
    AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;

    UPDATE B
    SET LastUpdated = GETDATE()
    FROM dbo.Blogs AS B
    INNER JOIN INSERTED AS I
        ON B.BlogId = I.BlogId
END

Trigger'ı EF Core oluşturmaz. Onu bir migration içinde migrationBuilder.Sql ile eklemeniz gerekir; nasıl yapıldığını migration özelleştirme yazısında anlattık. Property'yi de ValueGeneratedOnAddOrUpdate ile işaretlerseniz EF, değerin veritabanından geleceğini bilir.

Değer üretimini geçersiz kılmak

Bir property değer üretimi için yapılandırılmış olsa bile birçok durumda ona açık bir değer verebilirsiniz. Bunun işe yarayıp yaramadığı, yapılandırılan mekanizmaya bağlıdır: varsayılan değeri olan bir kolona açık değer yazabilirsiniz, ama hesaplanan kolona yazamazsınız. Açık değer vermek için property'yi tipinin CLR varsayılanından farklı bir değere ayarlamanız yeterlidir: string için null, int için 0, Guid için Guid.Empty varsayılandır. Bu kuralın tersinden okunması da önemlidir: değer üretimi yapılandırılmış bir property'de CLR varsayılan değeri, açık bir değer sayılmaz. Yani HasDefaultValue(3) verilmiş bir int property'de 0 değerini bilinçli olarak saklamak istiyorsanız bu kuralı tasarımınızda hesaba katmanız gerekir.

Eklemede ve güncellemede üretilen olarak işaretlenmiş property'lere açık değer verebilmek için ek bir ayar gerekir:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property(b => b.LastUpdated)
        .ValueGeneratedOnAddOrUpdate()
        .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
}

SQL Server IDENTITY kolonuna açık değer eklemek

SQL Server varsayılan olarak IDENTITY kolonlarına açık değer eklenmesine izin vermez. Örneğin taşıma sırasında eski kimlikleri korumanız gerekiyorsa, SaveChangesAsync'den önce IDENTITY_INSERT'i elle açmanız gerekir. Belgelerdeki örnek bağlantıyı kendisi açar, ayarı açıp kaydeder, ayarı kapatır ve finally bloğunda bağlantıyı kapatır:

csharp
using (var context = new ExplicitIdentityValuesContext())
{
    context.Blogs.Add(new Blog { BlogId = 100, Url = "http://blog1.somesite.com" });
    context.Blogs.Add(new Blog { BlogId = 101, Url = "http://blog2.somesite.com" });

    await context.Database.OpenConnectionAsync();
    try
    {
        await context.Database.ExecuteSqlRawAsync("SET IDENTITY_INSERT dbo.Blogs ON");
        await context.SaveChangesAsync();
        await context.Database.ExecuteSqlRawAsync("SET IDENTITY_INSERT dbo.Blogs OFF");
    }
    finally
    {
        await context.Database.CloseConnectionAsync();
    }
}

Belgeler bunun sağlayıcı içinde otomatik yapılması için bir özellik isteği bulunduğunu not eder; şimdilik işi elle yaparsınız. IDENTITY'nin başlangıç (seed) ve artış (increment) değerleri varsayılan olarak 1'dir ve UseIdentityColumn(seed: 10, increment: 10) ile değiştirilebilir.

Değer üretimini kapatmak

Yukarıdaki özel durumlar dışında property'lerde değer üretimi yoktur; değeri her zaman uygulama verir ve entity context'e eklenmeden önce atanmalıdır. Bazen de konvansiyonla kurulmuş üretimi kapatmak istersiniz; örneğin int anahtarı bir dış sistemden geliyorsa. [DatabaseGenerated(DatabaseGeneratedOption.None)] veya ValueGeneratedNever() bunu yapar.

csharp
public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int BlogId { get; set; }

    public string Url { get; set; }
}

modelBuilder.Entity<Blog>()
    .Property(b => b.BlogId)
    .ValueGeneratedNever();

Sequence: tablodan bağımsız sayı üretimi

Sequence, veritabanında benzersiz ve sıralı sayısal değerler üreten bir nesnedir. IDENTITY'den farkı belirli bir tabloya bağlı olmamasıdır; birden fazla tablo aynı sequence'tan değer çekebilir. Belgeler sequence'ların genellikle yalnızca ilişkisel veritabanlarında desteklendiğini, Azure Cosmos DB gibi ilişkisel olmayan bir veritabanında benzersiz değer üretimi için o veritabanının belgelerine bakmak gerektiğini belirtir. Sequence'ı modelde HasSequence ile tanımlar, property'ye HasDefaultValueSql ile bağlarsınız. Şema, başlangıç değeri ve artış miktarı da ayarlanabilir.

csharp
// Temel kullanim
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasSequence<int>("OrderNumbers");

    modelBuilder.Entity<Order>()
        .Property(o => o.OrderNo)
        .HasDefaultValueSql("NEXT VALUE FOR OrderNumbers");
}

// Sema, baslangic degeri ve artis miktari
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared")
        .StartsAt(1000)
        .IncrementsBy(5);
}

Sequence'tan değer alan SQL veritabanına özgüdür; NEXT VALUE FOR SQL Server'da çalışır, başka veritabanlarında başarısız olur. Model düzeyinde HasDefaultSchema verdiyseniz bunun sequence'ları da etkilediğini unutmayın. SQL Server belgeleri sequence'ı IDENTITY'ye bir alternatif olarak sunar; örneğin birden çok kolonun varsayılan değerini tek bir sequence'tan almak istediğiniz senaryolar için.

SQL Server: sıralı GUID ve rowversion

GUID birincil anahtarlar için SQL Server sağlayıcısı, SQL Server'ın NEWSEQUENTIALID fonksiyonuna benzer biçimde en uygun sıralı değerleri kendisi üretir. Değerin istemcide üretilmesi bazı senaryolarda daha verimlidir: o anahtara başvuran bağımlı bir kayıt da eklenirken veritabanının ürettiği değeri almak için ek bir gidiş dönüş gerekmez. Aynı sıralı GUID değerlerini anahtar olmayan bir property için de istiyorsanız HasValueGenerator(typeof(SequentialGuidValueGenerator)) kullanırsınız.

SQL Server'ın rowversion tipi satır her güncellendiğinde kendiliğinden değişir ve iyimser eşzamanlılık kontrolü için concurrency token olarak kullanılır. Bir byte[] property'sini [Timestamp] veya IsRowVersion() ile rowversion kolonuna eşlersiniz. Bu konunun bütünü, çakışma yakalama ve çözme ile birlikte concurrency ve rowversion yazısında anlatılıyor.

csharp
modelBuilder.Entity<Blog>().Property(b => b.Guid).HasValueGenerator(typeof(SequentialGuidValueGenerator));

modelBuilder.Entity<Person>()
    .Property(p => p.Version)
    .IsRowVersion();

SQLite: AUTOINCREMENT ve ROWID

SQLite sağlayıcısı, eklemede değer üretilen sayısal birincil anahtar kolonlarını konvansiyonla SQLite'ın AUTOINCREMENT özelliğiyle kurar. EF Core 10 ile bu davranış yapılandırmayla açılıp kapatılabilir hâle geldi ve value converter'ı olan property'lerde de desteklenir. Property bir dönüştürücü ile tamsayı olmayan bir tipten geliyorsa ya da konvansiyonu ezmek istiyorsanız UseAutoincrement() ile açıkça işaretlersiniz:

csharp
modelBuilder.Entity<Blog>()
    .Property(b => b.Id)
    .HasConversion<int>()
    .UseAutoincrement();

AUTOINCREMENT, SQLite'ın varsayılan anahtar üretim algoritması olan ROWID'e göre ek CPU, bellek, disk alanı ve disk G/Ç yükü getirir. ROWID'in dezavantajı ise silinen satırların değerlerini yeniden kullanabilmesidir. Senaryonuz bundan etkilenmiyorsa AUTOINCREMENT'i kapatıp SQLite'ın varsayılan davranışını kullanabilirsiniz. Bunun için meta veri API'sinde SetValueGenerationStrategy(SqliteValueGenerationStrategy.None) çağırırsınız ya da property'yi ValueGeneratedNever() ile değer üretilmeyen olarak işaretlersiniz. İkinci yolda değeri uygulama vermelidir; ama veritabanı tarafındaki varsayılan üretim yine de kapanmaz, EF dışındaki kullanımlar değer almaya devam edebilir. Belgelere göre üretimi tamamen kapatmak için kolon tipini INTEGER yerine INT yapmanız gerekir.

csharp
modelBuilder.Entity<Blog>()
    .Property(b => b.Id)
    .Metadata.SetValueGenerationStrategy(SqliteValueGenerationStrategy.None);

Sık yapılan hatalar

Sık yapılan hatalar: DateTime property'ye ValueGeneratedOnAdd verip değerin dolmasını..., Güncelleme zamanını kalıcı hesaplanan kolonla çözmeye çalışmak., Varsayılanı olan bir...
  • DateTime property'ye ValueGeneratedOnAdd verip değerin dolmasını beklemek. Bu ayar yalnızca bilgi verir; değer için HasDefaultValueSql gibi bir mekanizma gerekir.
  • Güncelleme zamanını kalıcı hesaplanan kolonla çözmeye çalışmak. Veritabanları genellikle bu fonksiyonlara izin vermez; trigger kullanın.
  • Varsayılanı olan bir kolona CLR varsayılan değerini bilerek yazmak. 0 veya null, EF tarafından değer verilmemiş sayılır.
  • Sequence SQL'ini sağlayıcıdan bağımsız sanmak. NEXT VALUE FOR SQL Server'a özgüdür.
  • Mevcut projede UseNamedDefaultConstraints'i habersiz açmak. Sonraki migration tüm default constraint'leri yeniden adlandırır.
  • IDENTITY kolonuna açık değer yazıp SaveChanges'ın başarılı olmasını beklemek. SQL Server'da IDENTITY_INSERT gerekir.

Sık Sorulan Sorular

HasDefaultValue ile C# tarafında property başlatmak arasındaki fark nedir?

HasDefaultValue değeri veritabanı şemasına koyar; EF dışında, örneğin elle yazılan bir SQL ile eklenen satırlar da bu değeri alır ve yeni kolon eklenirken mevcut satırlar doldurulur. C# tarafında property'yi başlatmak ise yalnızca uygulamanın oluşturduğu nesneleri etkiler. Hangisinin uygun olduğu, varsayılanın şemanın bir parçası olmasını isteyip istemediğinize bağlıdır.

Sanal mı kalıcı mı hesaplanan kolon seçmeliyim?

Sanal kolon yer kaplamaz ama her okumada hesaplanır; kalıcı kolon yazmada hesaplanır ve saklanır. İfade üzerinde filtreleyip index kullanmak istiyorsanız belgeler kalıcı kolon ve üzerine index önerir.

Sequence mi IDENTITY mi?

IDENTITY bir tabloya bağlıdır ve SQL Server'da sayısal anahtarlar için varsayılandır. Sequence tablodan bağımsızdır ve birden çok tablo ya da kolon tarafından paylaşılabilir. Belgeler sequence'ı, birden fazla kolonun değerini tek kaynaktan almak gibi senaryolar için alternatif olarak sunar.

SQLite tablolarımda neden AUTOINCREMENT var?

SQLite sağlayıcısı, tek kolonlu ve FK olmayan tamsayı anahtarlar için AUTOINCREMENT'i konvansiyonla kurar. ROWID'in silinen değerleri yeniden kullanması sizin için sorun değilse EF Core 10 ile bunu kapatabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: EF Core öğrenme rehberi · Primary key ve composite key · Concurrency ve rowversion · Migration özelleştirme ve ham SQL · Property ve kolon yapılandırma · EF Core 10 yenilikleri