İçeriğe geç
academia.sh

Ders 08 / 14

Yerel Veritabanı

Yerel veritabanı, uygulamanın verisini aygıt diskinde varlık ve veri erişim nesnesi ile tutar; şema değişince göç eski satırları yeni şemaya taşır. Ders göçten sonra bir okumanın yanıtının kökenini ve yeni sütunun eski satırlarda neye düştüğünü ölçer.

Yayın Güncelleme

Bu derste

  • Varlık, veri erişim nesnesi ve veritabanı sınıfının görevlerini ayırır
  • Şema sürümü değişince göçün eski satırları yeni şemaya taşıdığını açıklar
  • Göçten geçmiş bir satırın okumasının kökenini disk olarak ölçer
  • Göçte doldurulmayan yeni sütunun eski satırlarda varsayılana düştüğünü gösterir
İçindekiler

Önceki derste depo, kaynakları bir sıraya dizdi ve disk basamağı hazır bir eşlemdi. Platformda bu basamağın arkasında bir yerel veritabanı durur ve onun şeması uygulamayla birlikte büyür. Bu derste disk basamağının nasıl dolduğuna ve şema değişince eski satırların ne olduğuna bakıyoruz. Dersin sorusu önceki derslerle aynıdır: bu okumanın yanıtı nereden geliyor?

// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}

Varlık, Veri Erişim Nesnesi ve Veritabanı

Yerel veritabanı kütüphanesi üç parçadan oluşur. Varlık (entity), bir tablonun satırını anlatan sınıftır; her alanı bir sütundur. Veri erişim nesnesi (data access object, DAO), o tabloya yapılan sorguların ve yazmaların arayüzüdür. Veritabanı sınıfı varlıkları ve şema sürümünü bir araya getirir ve veri erişim nesnelerini sunar. Bundan sonra yalnızca DAO diyeceğiz.

// Platform kodu: AppDatabase
@Entity(tableName = "cart_item")
data class CartItemEntity(
    @PrimaryKey val productId: String,
    val quantity: Int,
    val stock: Int = 0,
)

@Dao
interface CartItemDao {
    @Query("SELECT * FROM cart_item WHERE productId = :id")
    suspend fun find(id: String): CartItemEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun save(item: CartItemEntity)
}

@Database(entities = [CartItemEntity::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
    abstract fun cartItemDao(): CartItemDao
}

Sürüm numarası şemanın kimliğidir. Uygulamanın yeni sürümü sütun eklediğinde numara 1’den 2’ye çıkar ve eski sürümle yazılmış satırlar diskte durmaya devam eder. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde veritabanı, bir disk basamağıdır; satırlar ise anahtar ve değer çiftleridir. Şema değişikliği bu çiftlerin anahtarlarını etkiler.

Göç Satırları Taşır

Sürüm 1’de sepetin adedi sepet.miktar sütunundaydı. Sürüm 2’de bu sütun sepet.adet oldu ve yeni bir sütun, urun.stok, eklendi. Göç (migration), eski sürümün satırlarını yeni sürümün şemasına taşıyan kuraldır. Platformda bunu bir sınıf yazar ve içinde şema komutları bulunur.

// Platform kodu: Migration1To2
val migration1To2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE cart_item RENAME COLUMN quantity TO count")
        db.execSQL("ALTER TABLE cart_item ADD COLUMN stock INTEGER NOT NULL DEFAULT 0")
    }
}
ALTER TABLE cart_item RENAME COLUMN quantity TO count;
ALTER TABLE cart_item ADD COLUMN stock INTEGER NOT NULL DEFAULT 0;

Platformda DEFAULT 0 ifadesi eski satırlara sıfır yazar. Modelde ise bu davranışı iki ayrı seçim olarak ayıracağız. Göç işlevi anahtarları yeniden adlandırır ve istenirse yeni sütunun değerini eski satırlara yazar.

// Migrate.kt — eski satirlari yeni semaya tasir, yeni sutunu istenirse doldurur
fun migrate(
    rows: Map<String, String>,
    renames: Map<String, String>,
    backfill: Map<String, String>,
): Map<String, String> =
    rows.map { (key, value) -> (renames[key] ?: key) to value }.toMap() + backfill

Ekran, sürüm 2’nin şemasına göre dört değer okur.

// Screen.kt — sema surumu 2'ye gore okunan dort deger
fun readCartScreen(disk: Tier): List<Read> {
    val chain = listOf(disk)
    return listOf(
        Read("profil-adi", "profil.ad", chain, "bilinmiyor"),
        Read("urun-fiyati", "urun.fiyat", chain, "bilinmiyor"),
        Read("sepet-adedi", "sepet.adet", chain, "0"),
        Read("urun-stogu", "urun.stok", chain, "0"),
    )
}

Önce göçten önceki hali ölçelim. Disk, sürüm 1 ile yazılmış üç satır taşır ve henüz kimse onları dönüştürmemiştir.

// Main.kt — goc yokken surum 2'nin sorgusu surum 1'in satirlarina bakar
fun main() {
    val v1Rows = mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "sepet.miktar" to "2")
    val reads = readCartScreen(Tier("disk", v1Rows))
    reads.forEach { it.show() }
    countOrigins(reads)
}
profil-adi: anahtar=profil.ad | deger=Ayse -> disk
urun-fiyati: anahtar=urun.fiyat | deger=120 -> disk
sepet-adedi: anahtar=sepet.adet | deger=0 -> varsayilan
urun-stogu: anahtar=urun.stok | deger=0 -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 2

profil.ad ve urun.fiyat adı değişmeyen sütunlardır, diskten okundu. Adedin diskte durduğu anahtar sepet.miktar’dı; sorgu sepet.adet’i aradığı için satır var olduğu hâlde bulunmadı ve yanıt varsayılana düştü. Veri kaybolmadı; yeni şema onu eski adıyla görmüyor.

Şimdi göçü uygulayalım. Yalnız yeniden adlandırma var, yeni sütun doldurulmuyor.

// Main.kt — goc adi degisen sutunu tasir
fun main() {
    val v1Rows = mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "sepet.miktar" to "2")
    val migrated = migrate(v1Rows, mapOf("sepet.miktar" to "sepet.adet"), emptyMap())
    val reads = readCartScreen(Tier("disk", migrated))
    reads.forEach { it.show() }
    countOrigins(reads)
}
profil-adi: anahtar=profil.ad | deger=Ayse -> disk
urun-fiyati: anahtar=urun.fiyat | deger=120 -> disk
sepet-adedi: anahtar=sepet.adet | deger=2 -> disk
urun-stogu: anahtar=urun.stok | deger=0 -> varsayilan
bellek: 0
disk: 3
ag: 0
varsayilan: 1

Adet artık diskten okunuyor ve değeri 2’dir. Bu dersin kökeni disktir: göçten geçmiş satırın yanıtı diske aittir. urun.stok ise sürüm 1’de hiç yazılmadığı için diskte yoktur.

VK1. Model, göçü tek adımlık bir eşleme olarak kurar: anahtarı yeniden adlandırır, istenirse bir değer ekler. Platformda göçün ara sürümlerden geçmesi, tablonun yeniden oluşturulması ve göç sırasında verinin tutarlılığını koruyan işlem sınırları modelde yoktur.

Sınırlayıcı Ölçüm

Yeni sütun eski satırlarda ne olur? Göç onu doldurmazsa eski satırda yoktur ve yukarıdaki ölçümde gördüğümüz gibi o alan varsayılana düşer. Sütunun varsayılan değeri Read’in dördüncü parametresidir. Göç DEFAULT 0 gibi bir kuralla sütunu doldurursa durum değişir.

// Main.kt — ayni deger, iki goc: doldurulmayan sutun varsayilan, doldurulan sutun disk
fun main() {
    val v1Rows = mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "sepet.miktar" to "2")
    val renames = mapOf("sepet.miktar" to "sepet.adet")
    val plans = listOf(
        "doldurmadan" to emptyMap<String, String>(),
        "doldurarak" to mapOf("urun.stok" to "0"),
    )
    for ((name, backfill) in plans) {
        println("goc: $name")
        val migrated = migrate(v1Rows, renames, backfill)
        readCartScreen(Tier("disk", migrated)).filter { it.key == "urun.stok" }.forEach { it.show() }
    }
}
goc: doldurmadan
urun-stogu: anahtar=urun.stok | deger=0 -> varsayilan
goc: doldurarak
urun-stogu: anahtar=urun.stok | deger=0 -> disk

İki göçte de urun.stok değeri 0’dır. Köken ise farklıdır: biri okuyucunun yedek değeri, öteki diskte yazılı bir satır. Kural değeri değil, anahtarı taşıyan ilk basamağı görür. Bunun yararı, bir sütunun hangi satırlarda yazıldığının, hangilerinde yalnızca öyle göründüğünün ayrılabilmesidir. Modeli kuran kişi doldurmanın değeri değiştirmediğini, ama yanıtın sahibini değiştirdiğini görür.

Bu ölçümün söylemediği şey, hangi göçün doğru olduğudur. Eski satırlara değer yazmak veritabanını büyütür ve göç süresini uzatır; yazmamak ise bu alanın okunduğu her yerde bir yedek değer gerektirir. Kural bu bedeli tartmaz.

VK2. Model, göçü ya uygulanmış ya uygulanmamış olarak ele alır. Platformda göçün yarıda kesilmesi, eksik bir göç tanımı olması ya da veritabanının eski sürüme geri alınması modelde yoktur; bu durumlarda platform hata verir ve model bu hatayı kurmaz.

Özet

  • Bu dersin kökeni: disk. Göçten geçmiş bir satırın okuması, disk basamağından yanıtlanır.
  • Varlık satırı, veri erişim nesnesi sorguyu, veritabanı sınıfı şema sürümünü taşır.
  • Göç eski satırları yeni şemaya taşır; taşınmamış satır yeni adla aranınca bulunmaz ve varsayılana düşer: disk 2, varsayılan 2.
  • Yeniden adlandırma göçüyle adet 2 değeriyle diskten okunur: disk 3, varsayılan 1.
  • Doldurulmayan yeni sütun varsayılana düşer, doldurulan disk olur; iki durumda da değer 0’dır, köken farklıdır.

Sonraki Adım

Bu derste disk, satırları ve sütunları olan bir veritabanıydı. Her veri bir tabloya girecek kadar yapılı değildir: tema, dil ve bildirim gibi küçük tercihler tek anahtarla yazılır. Sıradaki ders, Anahtar–Değer Saklama, hiç yazılmamış bir tercihin hangi kökenden okunduğunu ve kullanıcı yazınca kökenin nasıl değiştiğini ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat