התייחסות לנתונים מורכבים באמצעות 'חדר'

‫Room יכול להמיר סוגים פרימיטיביים וסוגים שעברו המרה, אבל הוא לא מאפשר הפניות לאובייקטים בין ישויות. במאמר הזה נסביר איך להשתמש בממירים של סוגים ולמה Room לא תומך בהפניות לאובייקטים.

שימוש בממירים של סוגים

לפעמים צריך שאפליקציה תשמור סוג נתונים מותאם אישית בעמודה אחת במסד נתונים. כדי לתמוך בסוגים מותאמים אישית, צריך לספק ממירים של סוגים. אלה פונקציות שמסבירות ל-Room איך להמיר סוגים מותאמים אישית לסוגים מוכרים ש-Room יכול לשמור, ואיך להמיר סוגים מוכרים לסוגים מותאמים אישית. כדי לזהות המרות של סוגים, משתמשים בהערה @ColumnTypeConverter.

נניח שאתם צריכים לשמור מופעים של Date במסד הנתונים של Room. ‫Room לא יכול לשמור אובייקטים של Date באופן מקורי, ולכן צריך להגדיר המרות סוגים:

object Converters {
    @ColumnTypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @ColumnTypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

בדוגמה הזו מוגדרות שתי פונקציות להמרת סוגים: אחת שממירה אובייקט Date לאובייקט Long, ואחת שממירה אובייקט Long בחזרה לאובייקט Date. ‫Room יכולה לשמור אובייקטים של Long, ולכן היא יכולה להשתמש בממירים האלה כדי לשמור אובייקטים של Date.

לאחר מכן מוסיפים את ההערה @ColumnTypeConverters לכיתה AppDatabase כדי ש-Room יוכל להשתמש בכיתת הממיר שהגדרתם:

@Database(entities = [User::class], version = 1)
@ColumnTypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

אחרי שמגדירים את ממירים מסוגים האלה, אפשר להשתמש בסוג המותאם אישית בישויות וב-DAO בדיוק כמו בסוגים פרימיטיביים:

@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val birthday: Date?
)

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE birthday = :targetDate")
    suspend fun findUsersBornOnDate(targetDate: Date): List<User>
}

בגלל שהוספתם את ההערה AppDatabase עם @ColumnTypeConverters בדוגמה הזו, אפשר להשתמש ב-Room בממיר הסוגים שהוגדר בכל מקום. כדי להגדיר את היקף ההמרה של סוגים לישויות ספציפיות או לאובייקטים מסוג DAO, מוסיפים את ההערה @ColumnTypeConverters לכיתות @Entity או @Dao.

הפעלה של כלי להמרת סוגי בקרה

בדרך כלל, Room יוצר מופעים של המרות סוגים בשבילכם. עם זאת, אם אתם צריכים להעביר תלויות נוספות למחלקות של המרות סוגים, האפליקציה שלכם צריכה לשלוט ישירות בהפעלה שלהן. במקרה כזה, מוסיפים את ההערה @ProvidedColumnTypeConverter למחלקה של הכלי להמרת נתונים:

@ProvidedColumnTypeConverter
class ExampleConverter {
    @ColumnTypeConverter
    fun stringToExample(string: String?): ExampleType? {
        return string?.let { ExampleType() }
    }

    @ColumnTypeConverter
    fun exampleToString(example: ExampleType?): String? {
        return example?.toString()
    }
}

בנוסף להצהרה על מחלקת ההמרה ב-@ColumnTypeConverters, צריך להשתמש בפונקציה RoomDatabase.Builder.addColumnTypeConverter כדי להעביר מופע של מחלקת ההמרה אל הבונה RoomDatabase:

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addColumnTypeConverter(exampleConverterInstance)
    .build()

למה Room לא מאפשרת הפניות לאובייקטים

הנקודה העיקרית: ב-Room, אי אפשר להשתמש בהפניות לאובייקטים בין מחלקות של ישויות. במקום זאת, אתם צריכים לבקש באופן מפורש את הנתונים שהאפליקציה שלכם צריכה.

מיפוי קשרים ממסד נתונים למודל האובייקטים המתאים הוא שיטה נפוצה שעובדת טוב מאוד בצד השרת. גם כשהתוכנה טוענת מאפיינים בזמן הגישה אליהם, השרת עדיין פועל בצורה טובה.

עם זאת, בצד הלקוח, טעינה מדורגת מהסוג הזה לא אפשרית כי היא מתרחשת בדרך כלל בשרשור UI, ושאילתת מידע בדיסק בשרשור UI יוצרת בעיות משמעותיות בביצועים. בדרך כלל, לשרשור UI יש כ-16 אלפיות השנייה כדי לחשב ולצייר את הפריסה המעודכנת של פעילות מסוימת. לכן, גם אם שאילתה נמשכת רק 5 אלפיות השנייה, עדיין סביר שהאפליקציה לא תספיק לצייר את הפריים, ויופיעו באפליקציה תקלות ויזואליות בולטות. יכול להיות שיידרש עוד יותר זמן להשלמת השאילתה אם מתבצעת במקביל עסקה נפרדת, או אם המכשיר מריץ משימות אחרות שדורשות הרבה משאבים מהדיסק. אבל אם לא משתמשים בטעינה עצלה, האפליקציה מאחזרת יותר נתונים ממה שהיא צריכה, וזה יוצר בעיות של צריכת זיכרון.

מיפויים של אובייקטים יחסיים בדרך כלל משאירים את ההחלטה הזו למפתחים, כדי שהם יוכלו לעשות את מה שהכי טוב לתרחישי השימוש באפליקציה שלהם. בדרך כלל המפתחים מחליטים לשתף את המודל בין האפליקציה שלהם לבין ממשק המשתמש. עם זאת, הפתרון הזה לא מתאים לשימוש נרחב, כי ככל שממשק המשתמש משתנה לאורך זמן, המודל המשותף יוצר בעיות שמפתחים מתקשים לצפות ולנפות.

לדוגמה, נניח שיש ממשק משתמש שטוען רשימה של Book אובייקטים, וכל ספר הוא אובייקט Author. יכול להיות שבתחילה תתכננו את השאילתות כך שישתמשו בטעינה עצלה כדי לאחזר מופעים של Book. השאילתות הראשונות של הנכס author מאחזרות נתונים ממסד הנתונים. בשלב מסוים, אתם מבינים שאתם צריכים להציג את שם המחבר גם בממשק המשתמש של האפליקציה. אפשר לגשת לשם הזה, כמו שמוצג בקטע הקוד הבא:

Text(text = book.author.name)

עם זאת, השינוי הזה, שנראה תמים, גורם לשאילתה של הטבלה Author ב-thread הראשי.

אם אתם שולחים שאילתה לגבי פרטי המחבר מראש אבל לא צריכים אותם, קשה לשנות את אופן טעינת הנתונים. לדוגמה, אם ממשק המשתמש של האפליקציה לא צריך יותר להציג מידע מסוים, האפליקציה טוענת נתונים שהיא לא מציגה, ובכך מבזבזת מקום חשוב בזיכרון.Author היעילות של האפליקציה שלכם תרד עוד יותר אם המחלקה Author מפנה לטבלה אחרת, כמו Books.

כדי להפנות לכמה ישויות בו-זמנית באמצעות Room, יוצרים אובייקט נתונים שמכיל כל ישות, ואז כותבים שאילתה שמצטרפת לטבלאות המתאימות. המודל המובנה הזה, בשילוב עם יכולות אימות השאילתות החזקות של Room, מאפשר לאפליקציה לצרוך פחות משאבים בזמן טעינת הנתונים, וכך לשפר את הביצועים של האפליקציה ואת חוויית המשתמש.