使用 Room 參照複雜資料

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

Room 可轉換原始和 Box 類型資料,但不允許實體之間的物件參照。本文說明如何使用類型轉換器,以及 Room 不支援物件參照的原因。

使用類型轉換器

有時候,應用程式需要將自訂資料類型儲存於單一資料庫欄位。透過提供類型轉換器,應用程式即可支援自訂類型。類型轉換器會告知 Room 如何在 Room 可保留的已知類型與自訂類型之間轉換的方法。請使用 @TypeConverter 註解來識別類型轉換器。

假設開發人員需要保留 Room 資料庫中 Date 的執行個體,而 Room 不知道如何保留 Date 物件,那就需要定義類型轉換器:

Kotlin

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

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

Java

public class Converters {
  @TypeConverter
  public static Date fromTimestamp(Long value) {
    return value == null ? null : new Date(value);
  }

  @TypeConverter
  public static Long dateToTimestamp(Date date) {
    return date == null ? null : date.getTime();
  }
}

此範例定義兩種轉換器方法,一種是將 Date 物件轉換成 Long 物件,另一種則是從 LongDate 的反向轉換。由於 Room 知道如何保留 Long 物件,因此可以使用這些轉換器保留 Date 物件。

接下來,請在 AppDatabase 類別註解 @TypeConverters,讓 Room 知道開發人員已定義的轉換器類別:

Kotlin

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

Java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

定義這些類型轉換器後,即可在實體和 DAO 中使用自訂類型,方法與使用原始類型一樣:

Kotlin

@Entity
data class User(private val birthday: Date?)

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

Java

@Entity
public class User {
  private Date birthday;
}

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

在這個範例中,因為 AppDatabase 已註解 @TypeConverters,所以 Room 可以在所有位置使用已定義的類型轉換器。不過,開發人員也可以使用 @TypeConverters 註解 @Entity@Dao 類別,進而將轉換器範圍限定為特定實體或 DAO。

控制類型轉換器初始化

一般而言,Room 會自動處理類型轉換器的執行個體化。但是,開發人員有時可能需要傳送額外的依附元件至類型轉換器類別,也就是說,應用程式需要直接控制類型轉換器的初始化。在這種情況下,請在轉換類別中註解 @ProvidedTypeConverter

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

接著,除了在 @TypeConverters 中宣告轉換器類別之外,也可以使用 RoomDatabase.Builder.addTypeConverter() 方法,將轉換器類別的執行個體傳送至 RoomDatabase 建構工具:

Kotlin

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Java

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build();

瞭解聊天室不接受參照物件的原因

要點:Room 不允許在實體類別之間參照物件。開發人員必須改為明確要求,以取得應用程式所需的資料。

常見做法是參照從資料庫至個別物件模型之間的關聯,這種做法在伺服器端相當有用。即使程式載入存取欄位,伺服器仍可正常運作。

但是,這種延遲載入不適用於用戶端,因為這類活動通常發生在 UI 執行緒上,而且使用 UI 執行緒查詢磁碟上的資訊,將會嚴重影響效能。UI 執行緒通常有大約 16 毫秒的時間,以計算及繪製活動更新的版面配置。因此,即便查詢只需 5 毫秒,應用程式在執行時,仍可能有時間不足的問題,導致效能明顯變慢。如果執行緒同時執行其他作業,或是裝置正在執行其他工作,因而占用大量磁碟空間,那麼就會需要更多時間才能完成查詢。如果開發人員未使用延遲載入功能,則應用程式擷取的資料可能會超乎原本所需,因而產生記憶體用量問題。

物件關係參照通常取決於開發人員,以便他們根據應用程式功能,決定最佳做法。一般而言,開發人員會決定共用應用程式和 UI 之間的模型。然而,UI 會隨著時間改變,共用模型也會產生問題,開發人員更無法預測並偵錯,因此這個解決方案無法一勞永逸。

比方說,假設 UI 會載入 Book 物件清單,而每本書都有 Author 物件。開發人員可以先設計查詢方法,使用延遲載入來讓 Book 執行個體擷取作者。第一次擷取 author 欄位時,應用程式會查詢資料庫。不久之後,就能看到應用程式的 UI 也需要顯示作者名稱。如要輕鬆存取該名稱,請參閱以下程式碼範例:

Kotlin

authorNameTextView.text = book.author.name

Java

authorNameTextView.setText(book.getAuthor().getName());

儘管這項變更並不明顯,卻會使主執行緒查詢 Author 資料表。

如果預先查詢作者資訊,而稍後就不再用到它,那麼之後會難以改變資料載入的方式。舉例來說,儘管應用程式的 UI 不再需要顯示 Author 資訊,應用程式依舊會快速載入這些資訊,就會浪費寶貴的記憶體空間。如果 Author 類別參照了其他如 Books 這類的資料表,應用程式的效率就會更慢。

如果要使用 Room 並同時參照多個實體,請建立並改用包含所有實體的 POJO,然後針對彙整的對應資料表編寫查詢。這款模型結構完善,加上 Room 強大的查詢驗證功能,載入資料時更節省資源,進而提升應用程式的效能和使用者體驗。