Room'un @Relation'ı: Android'de N+1 sorgu olmadan bire-çok veri sorgulamak
Room'un @Relation ek açıklamasına pratik bir rehber — kategoriler ve girişler gibi bire-çok veriyi N+1 sorgu ya da elle join olmadan modellemek.
Bir bütçe uygulamasının kategorileri vardır ve her kategorinin girişleri vardır. Bir kiler uygulamasının ürünleri vardır ve her ürünün bir tarama geçmişi vardır. Neredeyse her yerel öncelikli uygulamanın bir yerinde bu şekil vardır: bir satır birçok başka satıra sahiptir. Bunu Room’da yüklemenin saf yolu — ebeveynleri getir, sonra döngüye gir ve her ebeveynin çocuklarını getir — bekleyen bir N+1 sorgu hatasıdır. Room’un bunu doğru biçimde düzelten bir ek açıklaması var, ve insanların beklediğinden daha küçük: @Relation.
Yazmamanız gereken sorgu
Diyelim ki Granyn’in kategoriye göre harcama ekranını yapıyorsunuz. Bir Category tablonuz ve bir Entry tablonuz var, her giriş categoryId üzerinden kendi kategorisine geri işaret ediyor. İçgüdü, kategorileri getirmek, sonra döngüde her kategorinin girişlerini DAO’dan istemektir:
val categories = categoryDao.getAll()
val result = categories.map { category ->
category to entryDao.getByCategory(category.id) // one query per category
}
Bu N+1’dir: kategori listesi için bir sorgu, sonra her kategori için bir sorgu daha. Beş kategoriyle bu görünmezdir. Bir düzine kategoriye yayılmış bir yıllık geçmişle, bu her ekran yüklemesinde SQLite’a bir düzine gidiş-dönüştür, her biri hiçbir sebep olmadan kendi sorgu-planlama ek yükünü öder.
@Relation’ın gerçekte ürettiği şey
@Relation bunu sihirli biçimde bir SQL JOIN’e dönüştürmez. Bu veri şekli için yaptığı şey daha akıllıcadır: kaç ebeveyniniz olursa olsun toplam iki sorgu çalıştıran kod üretir. İlki ebeveynleri getirir. İkincisi tüm çocukları tek seferde getirir, tüm ebeveyn id’lerinden aynı anda kurulan bir WHERE categoryId IN (...) ile filtrelenmiş olarak.
Kotlin tarafı, bir ebeveyni ve çocuklarını tutan bir sarmalayıcı sınıftır:
data class CategoryWithEntries(
@Embedded val category: Category,
@Relation(
parentColumn = "id",
entityColumn = "categoryId",
)
val entries: List<Entry>,
)
@Embedded, Category’nin kendi sütunlarını sonuca düzleştirir. @Relation, ebeveyndeki hangi sütunun (id) çocuktaki hangi sütunla (categoryId) eşleştiğini söyler — Granyn’in şemasının zaten ifade ettiği aynı yabancı-anahtar ilişkisi, sadece elle yazılmış SQL yerine Room’un sorgu oluşturucusu için tanımlanmış hali.
DAO yöntemi, bir ekleme dışında düz bir sorguya yakındır:
@Transaction
@Query("SELECT * FROM categories")
fun getCategoriesWithEntries(): Flow<List<CategoryWithEntries>>
@Transaction burada önemlidir ve yanlışlıkla atlanması kolaydır. Onsuz, ebeveyn sorgusu ve toplu çocuk sorgusu iki bağımsız okuma olarak çalışır — ikisi arasında tabloya bir yazma inerse, birbiriyle kısaca çelişen bir kategori listesi ve girişler listesi alabilirsiniz. İkisini de bir işlemde sarmalamak, çiftin tek bir tutarlı anlık görüntüden okunduğunu garanti eder.
İnsanları hâlâ şaşırtan kısım: toplu işlemenin bir sınırı var
SQLite, tek bir ifadede izin verilen değişken sayısına bir üst sınır koyar — tarihsel olarak 999, son sürümlerde daha yüksek ama yine de sonlu. Bu sınırdan daha fazla ebeveyn satırınız varsa, Room başarısız olmaz; IN (...) cümlesini sessizce birden çok sorguya böler ve sonuçları geri dikişler. Bir kategori listesi için bu asla olmayacak, ama aynı deseni ebeveyni binlerce olan bir yerde uygularsanız (diyelim ki bir ürün kataloğu), sorgu sayısı sessizce tam olarak 2 olmaktan çıkar. “İki sorgu”nun her ölçekte katı bir garanti olduğunu varsaymadan önce bilinmeye değer.
@Relation salt okunurdur
Üretilen yöntem yalnızca okumalar için birleşik nesneyi kurar. CategoryWithEntries’i bir birim olarak anlayan bir @Insert ya da @Update karşılığı yoktur — bir Category’yi yine CategoryDao üzerinden, bir Entry’yi yine EntryDao üzerinden eklersiniz, ilişki hiç yokmuş gibi. @Relation bir kalıcılık modeli değil, sorgu-zamanı kolaylığıdır. Sarmalayıcı sınıfı yazmalar için yeniden kullanmaya çalışmak, insanların onun tarafından kafasının en sık karıştığı yoldur.
Ne zaman tamamen atlanmalı
Her bire-çok okuma @Relation’ın arkasına ait değildir. Ekran gerçekten her girişe ihtiyaç duyuyorsa — bir kategorinin işlem listesi gibi — doğru araçtır: doğru toplu işlenmiş iki sorgu, N+1 yok. Ama ihtiyacınız olan tek şey bir sayıysa — bir pasta grafiği için bu ayın kategori başına toplamı gibi — bunları Kotlin’de toplamak için her Entry’yi belleğe yüklemek boşa harcanan bir iştir. Ham bir toplama sorgusu, satırları hiç somutlaştırmadan aynı işi yapar:
@Query("""
SELECT categoryId, SUM(amountMinor) AS total
FROM entries
WHERE at BETWEEN :start AND :end
GROUP BY categoryId
""")
fun monthlyTotals(start: Long, end: Long): Flow<List<CategoryTotal>>
Genel kural: arayüz gerçek çocuk satırlarına ihtiyaç duyduğunda @Relation kullanın, onlardan türetilen tek ihtiyacınız bir sayı olduğu anda bir GROUP BY sorgusuna inin. Bir toplamı hesaplamak için tam bir nesne grafiği yüklemek, yerel veritabanının aşırı-getirme versiyonudur, ve doğrudan sorarsanız SQLite toplamayı sizin için seve seve yapar, Kotlin’e sormak yerine.
@Relation, Room’un geri kalanının hak ettiği aynı sebeple hak ediyor bu yeri: bütün bir doğruluk hatası kategorisini — N+1 döngüsünü — join’i elle yazmanızı istemeden ortadan kaldırıyor. Doğru toplu işlenmiş iki sorgu, bir işlemin içine sarmalanmış. Bütün numara bu.
// İlgili okumalar
Günlükten dahası
2026'da Android'de Room TypeConverters: şemanı bozmadan enum, tarih ve liste saklamak
Android'de Room TypeConverters için pratik bir rehber — enum'lar, Instant/LocalDate ve listeler — ve bir converter'ı sessiz bir veri bozulması hatasına dönüştüren hatalar.
2026'da Room veritabanı index'leri: gerçekten yavaş olan sorguyu bulmak ve düzeltmek
Android'de Room/SQLite veritabanını index'lemeye pratik bir rehber — EXPLAIN QUERY PLAN okumak, tahmin etmeden @Index eklemek ve bir index'i sessizce etkisiz kılan hatalar.
Room'da tam metin arama: 2026'da yerel öncelikli bir Android uygulamasına anında arama eklemek
Room'un FTS4 desteğine pratik bir rehber — sanal bir arama tablosu kurmak, onu tetikleyicilerle senkron tutmak ve FTS5'in neden elle bir migration gerektirdiği.