使用 Room 參照複雜資料

Room 可轉換原始和裝箱類型的資料,但不允許在實體之間進行物件參照。本文將說明如何使用類型轉換器,以及為何 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 物件。

接下來,請將 @TypeConverters 註解新增到 AppDatabase 類別,讓 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);
}

在此範例中,由於您已為 @TypeConverters 加入 AppDatabase 註解,因此 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 不允許物件參照的原因

要點: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 強大的查詢驗證功能,讓應用程式載入資料時消耗的資源更少,可提升應用程式效能以及使用者體驗。