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.
SQLite yalnızca beş depolama sınıfı bilir: NULL, INTEGER, REAL, TEXT, BLOB. Gerçek bir uygulamanın veri modelinde neredeyse hiçbir şey böyle görünmez. Tekrarlayan bir abonelikte bir faturalama Frequency enum’u vardır. Bir kiler öğesinin bir son kullanma tarihi vardır. Bir bütçe kaydı bir etiket listesi taşıyabilir. Room’un @TypeConverter’ı bu iki dünya arasındaki köprüdür ve o kadar küçük bir kod parçasıdır ki insanlar onu bir kez yazar, bir daha düşünmeyi bırakır ve iki yıl sonra bir sütunun anlamını sessizce yeniden şekillendiren bir converter’la baş başa kalır.
Üç yerel-öncelikli uygulamada converter’ların bakımını yapıyorum — Subly’nin tekrarlayan faturalama tarihleri, Stocky’nin son kullanma takibi ve Granyn’deki kategori enum’ları — ve gerçekten gönderdiğim hataların hepsi aynı birkaç yanlıştan geldi. İşte bunlardan nasıl kaçınılır.
Temel biçim
Bir converter, veritabanına kayıtlı, saf bir fonksiyon çiftidir:
class Converters {
@TypeConverter
fun fromFrequency(value: Frequency): String = value.name
@TypeConverter
fun toFrequency(value: String): Frequency = Frequency.valueOf(value)
}
@Database(entities = [Subscription::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()
Sözleşmenin tamamı bu: her tip için, her yön için bir fonksiyon. Room, bir entity alanının tipi doğal olarak anlamadığı bir tip olduğunda bunları otomatik olarak çağırır. Tuzak, her tip için makul görünen uygulamada gizlidir.
Enum’lar: ismi saklayın, asla ordinal’i değil
Enum.ordinal cazip gelir çünkü bir Int’tir ve Room onu sıfır dönüştürme koduyla doğal olarak saklar. Aynı zamanda bir mayındır: ordinal, enum’un kaynak dosyasındaki konumundan başka bir şey değildir. Frequency durumlarını yeniden sıralayın veya MONTHLY ile YEARLY arasına QUARTERLY ekleyin, var olan her satır sessizce yanlış değere işaret etsin. Hiçbir şey fırlatılmaz. Hata, haftalık faturalanan bir aboneliğin yıllık olarak görünmesidir — bunu bir test değil, bir kullanıcı keşfeder.
enum class Frequency { WEEKLY, MONTHLY, QUARTERLY, YEARLY }
@TypeConverter
fun fromFrequency(value: Frequency): String = value.name
@TypeConverter
fun toFrequency(value: String): Frequency = Frequency.valueOf(value)
value.name’i TEXT olarak saklamak satır başına birkaç ekstra bayta mal olur ve yeniden sıralamaya karşı bağışıktır. Geriye kalan tek gerçek tehlike bir durumu yeniden adlandırmaktır — bunu, saklanan string’leri yeniden yazan elle yazılmış bir migrasyonla yapın, tıpkı başka herhangi bir sütun düzeyindeki veri değişikliğini ele alacağınız gibi.
Tarihler: tek bir gösterim seçin ve asla yerel saati saklamayın
Klasik Room tarih hatası converter’ın kendisi değildir, tutarsızlıktır: kod tabanının bir kısmı LocalDateTime’ı cihaz-yerel saat varsayarak dönüştürür, bir başka kısmı UTC varsayar ve İstanbul’daki bir kullanıcı için doğru olan bir tarih, aynı kullanıcı için saat dilimi geçen bir uçuştan sonra saatlerce kayar. Cihazlar ve saat dilimleri arasında doğru şekilde karşılaştırılması veya sıralanması gereken her şey için, yerel bir tarih-saat değil, bir an (instant) saklayın:
@TypeConverter
fun fromInstant(value: Instant?): Long? = value?.toEpochMilli()
@TypeConverter
fun toInstant(value: Long?): Instant? = value?.let(Instant::ofEpochMilli)
INTEGER olarak Long epoch-milisaniye, ham SQL’de doğru şekilde sıralanır (satırları önce Kotlin’e yüklemeden ORDER BY ve aralık sorguları için kullanışlıdır) ve saklanan değere gömülü bir saat dilimi belirsizliği yoktur — saat dilimi yalnızca görüntüleme zamanında, ait olduğu yer olan UI katmanında önemlidir. Alan gerçekten zaman bileşeni olmayan bir takvim tarihiyse — mesela bir kiler öğesinin son kullanma tarihi — bunun yerine ISO-8601 bir TEXT string’i (2026-09-14) olarak saklayın. Bu hâlâ string olarak sıralanabilir ve hiçbir zaman bir zaman anı olmamış bir şey için bir zaman damgasının yanlış hassasiyetinden kaçınır.
Listeler ve koleksiyonlar: neyden vazgeçtiğinizi bilin
Bir List<String> saklamak genellikle converter içinde JSON’a serileştirmek anlamına gelir:
@TypeConverter
fun fromTags(value: List<String>): String = Json.encodeToString(value)
@TypeConverter
fun toTags(value: String): List<String> = Json.decodeFromString(value)
Bu çalışır ve küçük, nadiren sorgulanan listeler için — bir bütçe kaydındaki birkaç serbest biçimli etiket gibi — pragmatik seçimdir. Ama bu bilinçli olarak yaptığınız bir takastır, bedava bir kolaylık değil: bir sütun içindeki JSON değeri SQL için opaktır. Bir etiket üzerinde WHERE yapamazsınız, onu indeksleyemezsiniz, ona karşı join yapamazsınız. Bir “şeyler listesi”nin sorgulanması, filtrelenmesi veya başka verilerle ilişkilendirilmesi gerektiği an, artık bir converter sorunu değildir — eksik bir tablodur. Bir join tablosuyla düzgün bir @Relation, önceden daha fazla kurulum maliyetine mal olur ve bir sorgunun “her abonelik, sonra etiketleri Kotlin’de filtrele” yerine “work etiketli her abonelik” ihtiyacı duyduğu ilk anda kendini amorti eder.
Çoğu converter hatasını gönderilmeden önce yakalayan iki kural
Converter’lar saf ve total olmalıdır. Elinizden geldiğince I/O yok, Clock.System.now() yok, beklenmeyen girdide fırlatma yok — daha eski bir uygulama sürümü tarafından saklanmış bir değerde fırlatan bir converter, tek bir bozuk satırı, o tabloya dokunan her açılışta bir çökmeye dönüştürür. Var olan veriyi okuyan her şey için, fırlatılan bir exception yerine güvenli bir yedek değeri (varsayılan bir enum durumu, null bir tarih) tercih edin.
Bir converter’ın depolama biçimi, Room’un şema dışa aktarımı onu görmese de, şemanızın bir parçasıdır. AutoMigration sütun tiplerini ve isimlerini karşılaştırır — bir converter’ı Instant’ı milisaniye olarak saklamaktan bir ISO string’i olarak saklamaya değiştirdiğinizden haberi yoktur. Bu değişiklik, saklanan verinin diğer her yeniden şekillendirmesiyle aynı elle yazılmış migrasyon ve MigrationTestHelper muamelesine ihtiyaç duyar, çünkü SQLite’ın bakış açısından bir TEXT sütunu eskiden başka bir şey anlamına geldiğini bilmez.
Bu ikisini doğru yapın, enum’ları isim olarak saklamaya devam edin, zaman damgalarını UTC epoch-milisaniye olarak tutun ve JSON’a uzanmadan önce gerçek bir tabloya uzanın — converter katmanı, hataların saklandığı yer olmaktan çıkar.
// İlgili okumalar
Günlükten dahası
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'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.
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.