Room może konwertować typy proste i opakowane, ale nie zezwala na odwołania do obiektów między encjami. Dowiedz się, jak używać konwerterów typów i dlaczego Room nie obsługuje odwołań do obiektów.
Używanie konwerterów typów
Czasami musisz zapisać w aplikacji niestandardowy typ danych w jednej kolumnie bazy danych. Obsługę typów niestandardowych zapewniają konwertery typów. Są to funkcje, które informują Room, jak konwertować typy niestandardowe na znane typy, które Room może utrwalać, i z powrotem. Konwertery typów identyfikuje się za pomocą adnotacji
@ColumnTypeConverter.
Załóżmy, że musisz utrwalać instancje Date w
bazie danych Room. Room nie może natywnie utrwalać obiektów Date, dlatego musisz zdefiniować konwertery typów:
object Converters { @ColumnTypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } } @ColumnTypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time } }
Ten przykład definiuje 2 funkcje konwertera typów: jedną, która konwertuje obiekt Date na obiekt Long, i drugą, która konwertuje obiekt Long z powrotem na obiekt Date. Ponieważ Room może utrwalać obiekty Long, może używać tych konwerterów do utrwalania obiektów Date.
Następnie dodaj @ColumnTypeConverters
adnotację do klasy AppDatabase, aby Room mógł używać zdefiniowanej klasy konwertera:
@Database(entities = [User::class], version = 1) @ColumnTypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
Po zdefiniowaniu tych konwerterów typów możesz używać typu niestandardowego w encjach i DAO tak samo jak typów prostych:
@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> }
Ponieważ w tym przykładzie klasa AppDatabase została oznaczona adnotacją @ColumnTypeConverters, Room może używać zdefiniowanego konwertera typów wszędzie. Aby ograniczyć zakres konwerterów typów do określonych encji lub DAO, oznacz klasy @Entity lub @Dao adnotacją @ColumnTypeConverters.
Kontrolowanie inicjowania konwertera typów
Zwykle Room tworzy instancje konwerterów typów. Jeśli jednak musisz przekazać dodatkowe zależności do klas konwerterów typów, aplikacja musi bezpośrednio kontrolować ich inicjowanie. W takim przypadku oznacz klasę konwertera
adnotacją @ProvidedColumnTypeConverter:
@ProvidedColumnTypeConverter class ExampleConverter { @ColumnTypeConverter fun stringToExample(string: String?): ExampleType? { return string?.let { ExampleType() } } @ColumnTypeConverter fun exampleToString(example: ExampleType?): String? { return example?.toString() } }
Oprócz zadeklarowania klasy konwertera w @ColumnTypeConverters użyj
funkcji RoomDatabase.Builder.addColumnTypeConverter, aby przekazać instancję klasy konwertera do konstruktora RoomDatabase:
val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name") .addColumnTypeConverter(exampleConverterInstance) .build()
Dlaczego Room nie zezwala na odwołania do obiektów
Najważniejsze informacje: Room nie zezwala na odwołania do obiektów między klasami encji. Zamiast tego musisz wyraźnie poprosić o dane, których potrzebuje Twoja aplikacja.
Mapowanie relacji z bazy danych na odpowiedni model obiektów jest powszechną praktyką i bardzo dobrze sprawdza się po stronie serwera. Nawet gdy program wczytuje właściwości w miarę ich używania, serwer nadal działa dobrze.
Po stronie klienta ten typ leniwego ładowania nie jest jednak możliwy, ponieważ zwykle występuje w wątku UI, a wysyłanie zapytań o informacje na dysku w wątku UI powoduje znaczne problemy z wydajnością. Wątek UI ma zwykle około 16 ms na obliczenie i narysowanie zaktualizowanego układu aktywności, więc nawet jeśli zapytanie trwa tylko 5 ms, prawdopodobnie aplikacja nie zdąży narysować klatki, co spowoduje zauważalne błędy wizualne. Wykonanie zapytania może zająć jeszcze więcej czasu, jeśli równolegle działa osobna transakcja lub jeśli urządzenie wykonuje inne zadania wymagające intensywnego korzystania z dysku. Jeśli jednak nie używasz leniwego wczytywania, aplikacja pobiera więcej danych niż potrzebuje, co powoduje problemy z wykorzystaniem pamięci.
Mapowania obiektowo-relacyjne zwykle pozostawiają tę decyzję deweloperom, aby mogli oni wybrać najlepsze rozwiązanie dla przypadków użycia aplikacji. Deweloperzy zwykle decydują się na udostępnienie modelu między aplikacją a interfejsem. To rozwiązanie nie jest jednak skalowalne, ponieważ w miarę zmian interfejsu udostępniony model powoduje problemy, których deweloperzy nie mogą przewidzieć ani debugować.
Rozważmy na przykład interfejs, który wczytuje listę obiektów Book, a każdy z nich ma obiekt Author. Możesz początkowo zaprojektować zapytania tak, aby używały leniwego wczytywania, aby instancje Book pobierały autora. Pierwsze pobranie właściwości author powoduje wysłanie zapytania do bazy danych. Po pewnym czasie zdajesz sobie sprawę, że musisz też wyświetlać nazwę autora w interfejsie aplikacji. Możesz uzyskać dostęp do tej nazwy, jak pokazano w tym fragmencie kodu:
Text(text = book.author.name)
Ta pozornie niewinna zmiana powoduje jednak wysłanie zapytania do tabeli Author w wątku głównym.
Jeśli wysyłasz zapytanie o informacje o autorze z wyprzedzeniem, ale nie są one potrzebne, trudno jest zmienić sposób wczytywania danych. Jeśli na przykład interfejs aplikacji nie musi już wyświetlać informacji o Author, aplikacja wczytuje dane, których nie wyświetla, marnując cenną pamięć. Wydajność aplikacji jeszcze bardziej się pogarsza, jeśli klasa Author odwołuje się do innej tabeli, np. Books.
Aby jednocześnie odwoływać się do wielu encji za pomocą Room, utwórz obiekt danych zawierający każdą encję, a następnie napisz zapytanie, które łączy odpowiednie tabele. Ten dobrze skonstruowany model w połączeniu z niezawodnymi funkcjami sprawdzania zapytań w Room pozwala aplikacji zużywać mniej zasobów podczas wczytywania danych, co zwiększa wydajność aplikacji i poprawia wrażenia użytkownika.