Tham chiếu dữ liệu phức tạp bằng Room

Room cung cấp chức năng chuyển đổi giữa loại nguyên thuỷ và loại đóng hộp nhưng không cho phép tham chiếu đối tượng giữa các thực thể. Tài liệu này giải thích cách sử dụng trình chuyển đổi loại và lý do Room không hỗ trợ các tham chiếu đối tượng.

Sử dụng các trình chuyển đổi loại

Đôi khi, bạn cần ứng dụng của mình lưu trữ một loại dữ liệu tuỳ chỉnh trong một cột cơ sở dữ liệu. Bạn hỗ trợ các loại tuỳ chỉnh bằng cách cung cấp trình chuyển đổi loại. Đây là các phương thức cho Room biết cách chuyển đổi loại tuỳ chỉnh thành và từ các loại đã biết mà Room có thể lưu trữ. Bạn nhận dạng các trình chuyển đổi loại bằng cách sử dụng chú giải @TypeConverter.

Giả sử bạn cần lưu trữ các bản sao Date trong cơ sở dữ liệu của Room. Room không biết cách lưu trữ đối tượng Date, vì vậy, bạn cần xác định trình chuyển đổi loại:

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();
  }
}

Ví dụ này xác định hai phương thức trình chuyển đổi loại: một phương thức chuyển đổi đối tượng Date thành đối tượng Long và một phương thức chuyển đổi ngược lại từ Long thành Date. Vì Room biết cách lưu trữ các đối tượng Long, nên có thể sử dụng những trình chuyển đổi này để lưu trữ đối tượng Date.

Tiếp theo, bạn thêm chú giải @TypeConverters vào lớp AppDatabase để Room biết về lớp trình chuyển đổi mà bạn đã xác định:

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();
}

Với những trình chuyển đổi loại đã xác định này, bạn có thể sử dụng loại tuỳ chỉnh trong các thực thể và DAO của mình giống như cách bạn sử dụng các loại nguyên thuỷ:

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);
}

Trong ví dụ này, Room có thể sử dụng trình chuyển đổi loại đã xác định ở mọi nơi vì bạn đã chú giải AppDatabase bằng @TypeConverters. Tuy nhiên, bạn cũng có thể giới hạn các trình chuyển đổi loại sang các thực thể hoặc DAO cụ thể bằng cách chú giải các lớp @Entity hoặc@Dao bằng @TypeConverters.

Kiểm soát khởi chạy trình chuyển đổi loại

Thông thường, Room xử lý việc tạo bản sao của trình chuyển đổi loại cho bạn. Tuy nhiên, đôi khi bạn có thể cần phải truyền các phần phụ thuộc bổ sung vào các lớp trình chuyển đổi loại của mình, nghĩa là bạn cần ứng dụng của mình kiểm soát trực tiếp việc khởi chạy các trình chuyển đổi loại. Trong trường hợp đó, hãy chú giải lớp trình chuyển đổi của bạn bằng @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) {
    ...
  }
}

Sau đó, ngoài việc khai báo lớp trình chuyển đổi của bạn trong @TypeConverters, hãy sử dụng phương thức RoomDatabase.Builder.addTypeConverter() để truyền một bản sao lớp trình chuyển đổi của bạn đến trình tạo RoomDatabase:

Kotlin

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

Java

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

Hiểu lý do Room không cho phép tham chiếu đối tượng

Điểm chính cần lưu ý: Room không cho phép tham chiếu đối tượng giữa các lớp thực thể. Thay vào đó, bạn phải yêu cầu rõ ràng dữ liệu mà ứng dụng của bạn cần.

Lập bản đồ các mối quan hệ từ cơ sở dữ liệu với mô hình đối tượng tương ứng là một phương thức phổ biến và hoạt động rất hiệu quả ở phía máy chủ. Ngay cả khi chương trình tải các trường khi truy cập các trường đó, máy chủ vẫn hoạt động tốt.

Tuy nhiên, về phía máy khách, loại tải từng phần này không khả thi vì thường xảy ra trên luồng giao diện người dùng và việc truy vấn thông tin trong ổ đĩa trên luồng giao diện người dùng tạo ra các vấn đề đáng kể về hiệu suất. Luồng giao diện người dùng thường có khoảng 16 mili giây để tính toán và vẽ bố cục cập nhật của hoạt động, vì vậy, ngay cả khi một truy vấn chỉ mất 5 mili giây, thì vẫn có khả năng ứng dụng của bạn sẽ hết thời gian để vẽ khung hình, gây ra những sự cố nhỏ về hình ảnh đáng chú ý. Còn có thể mất nhiều thời gian hơn để hoàn tất truy vấn nếu có một giao dịch riêng đang chạy song song hoặc nếu thiết bị đang chạy các nhiệm vụ khác nặng về ổ đĩa. Tuy nhiên, nếu bạn không sử dụng tính năng tải từng phần, thì ứng dụng của bạn sẽ tìm nạp nhiều dữ liệu hơn mức cần thiết, gây ra các vấn đề về mức tiêu thụ bộ nhớ.

Các bản đồ quan hệ giữa các đối tượng thường để lại quyết định này cho nhà phát triển để họ có thể làm bất cứ điều gì tốt nhất cho trường hợp sử dụng của ứng dụng. Nhà phát triển thường quyết định chia sẻ mô hình giữa ứng dụng của họ và giao diện người dùng. Tuy nhiên, giải pháp này sẽ không mở rộng quy mô vì giao diện người dùng sẽ thay đổi theo thời gian, nên mô hình dùng chung sẽ gây ra những sự cố khiến các nhà phát triển khó dự đoán và gỡ lỗi.

Ví dụ: xem xét một giao diện người dùng tải danh sách các đối tượng Book, trong đó mỗi cuốn sách có một đối tượng Author. Ban đầu, bạn có thể thiết kế các truy vấn của mình để sử dụng tải từng phần nhằm có được các bản sao Book truy xuất tác giả. Lần truy xuất trường author đầu tiên truy vấn cơ sở dữ liệu. Sau một thời gian, bạn nhận thấy rằng bạn cũng cần hiển thị tên tác giả trong giao diện người dùng của ứng dụng. Bạn có thể dễ dàng truy cập tên này, như trong đoạn mã sau:

Kotlin

authorNameTextView.text = book.author.name

Java

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

Tuy nhiên, sự thay đổi có vẻ như vô hại này đã khiến bảng Author cần được truy vấn trên luồng chính.

Nếu bạn truy vấn thông tin tác giả trước thời hạn, bạn sẽ khó thay đổi cách tải dữ liệu nếu không cần dữ liệu đó nữa. Ví dụ: nếu giao diện người dùng của ứng dụng không còn cần hiển thị thông tin Author nữa, ứng dụng của bạn sẽ tải dữ liệu một cách hiệu quả mà ứng dụng không còn hiển thị nữa, từ đó làm lãng phí dung lượng bộ nhớ có giá trị. Hiệu suất của ứng dụng sẽ giảm xuống nếu lớp Author tham chiếu đến một bảng khác, chẳng hạn như Books.

Để tham chiếu đến nhiều thực thể cùng lúc bằng Room, thay vào đó, bạn cần tạo một JOJO chứa từng thực thể, sau đó ghi một truy vấn sắp xếp các bảng tương ứng. Mô hình có cấu trúc tốt này, kết hợp với khả năng xác thực truy vấn mạnh mẽ của Room, cho phép ứng dụng của bạn tiêu thụ ít tài nguyên hơn khi tải dữ liệu, cải thiện hiệu suất và trải nghiệm người dùng cho ứng dụng.