ارجاع داده های پیچیده با استفاده از Room

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های خاص محدود کنید، کلاس‌های @Entity یا @Dao خود را با @ColumnTypeConverters حاشیه‌نویسی کنید.

مقداردهی اولیه مبدل نوع کنترلی

معمولاً، 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 thread) رخ می‌دهد و پرس‌وجوی اطلاعات روی دیسک در نخ رابط کاربری (UI thread) مشکلات عملکردی قابل توجهی ایجاد می‌کند. نخ رابط کاربری معمولاً حدود ۱۶ میلی‌ثانیه برای محاسبه و ترسیم طرح‌بندی به‌روز شده یک فعالیت زمان دارد، بنابراین حتی اگر یک پرس‌وجو فقط ۵ میلی‌ثانیه طول بکشد، باز هم احتمال دارد که برنامه شما برای ترسیم قاب زمان کافی نداشته باشد و باعث ایجاد اشکالات بصری قابل توجه شود. اگر یک تراکنش جداگانه به صورت موازی در حال اجرا باشد، یا اگر دستگاه وظایف فشرده دیسک دیگری را اجرا کند، تکمیل پرس‌وجو می‌تواند زمان بیشتری طول بکشد. با این حال، اگر از بارگذاری تنبل استفاده نکنید، برنامه شما داده‌های بیشتری از آنچه نیاز دارد را دریافت می‌کند و مشکلات مصرف حافظه ایجاد می‌کند.

نگاشت‌های شیء-رابطه‌ای معمولاً این تصمیم را به توسعه‌دهندگان واگذار می‌کنند تا بتوانند هر کاری را که برای موارد استفاده برنامه‌شان بهتر است انجام دهند. توسعه‌دهندگان معمولاً تصمیم می‌گیرند مدل را بین برنامه خود و رابط کاربری به اشتراک بگذارند. با این حال، این راه‌حل به خوبی مقیاس‌پذیر نیست، زیرا با تغییر رابط کاربری در طول زمان، مدل مشترک مشکلاتی ایجاد می‌کند که پیش‌بینی و اشکال‌زدایی آنها برای توسعه‌دهندگان دشوار است.

برای مثال، یک رابط کاربری (UI) را در نظر بگیرید که لیستی از اشیاء Book ) را بارگذاری می‌کند، که هر کتاب دارای یک شیء Author (Author) است. شما ممکن است در ابتدا کوئری‌های خود را طوری طراحی کنید که از بارگذاری تنبل (lazy loading) استفاده کنند تا نمونه‌هایی از Book نویسنده را بازیابی کنند. اولین بازیابی ویژگی author ، از پایگاه داده پرس و جو می‌کند. مدتی بعد، متوجه می‌شوید که باید نام نویسنده را در رابط کاربری برنامه خود نیز نمایش دهید. می‌توانید به این نام دسترسی داشته باشید، همانطور که در قطعه کد زیر نشان داده شده است:

Text(text = book.author.name)

با این حال، این تغییر به ظاهر بی‌ضرر باعث می‌شود که جدول Author در thread اصلی مورد پرسش قرار گیرد.

اگر اطلاعات نویسنده را از قبل جستجو کنید اما به آن نیازی نداشته باشید، تغییر نحوه بارگذاری داده‌ها دشوار است. برای مثال، اگر رابط کاربری برنامه شما دیگر نیازی به نمایش اطلاعات Author نداشته باشد، برنامه شما عملاً داده‌هایی را بارگذاری می‌کند که نمایش نمی‌دهد و فضای ارزشمند حافظه را هدر می‌دهد. اگر کلاس Author به جدول دیگری مانند Books ارجاع دهد، کارایی برنامه شما حتی بیشتر کاهش می‌یابد.

برای ارجاع همزمان به چندین موجودیت با استفاده از Room، یک شیء داده ایجاد کنید که شامل هر موجودیت باشد و سپس یک پرس‌وجو بنویسید که جداول مربوطه را به هم متصل کند. این مدل خوش‌ساخت، همراه با قابلیت‌های قوی اعتبارسنجی پرس‌وجو در Room، به برنامه شما اجازه می‌دهد هنگام بارگذاری داده‌ها منابع کمتری مصرف کند و عملکرد برنامه و تجربه کاربری شما را بهبود بخشد.