+3

Android Room Database Tips

Chào mọi người, chắc hẳn trong chúng ta nếu triển khai database của Android trong thời điểm hiện tại, chúng ta sẽ nghĩ ngay đến việc sử dụng Room. Chính vì vậy, hôm nay mình xin chia sẻ một số Tips nhỏ trong việc sử dụng Room đến mọi người.

1. Thiết lập ràng buộc giữa các Entities thông qua ForeignKey

Mặc dù Room không hỗ trợ trực tiếp ràng buộc giữa các mối quan hệ, nhưng nó cho phép bạn xác định các ràng buộc Foreign keys giữa các Entities.

Thông qua annotation @ForeignKey, một phần trong bộ annotation của @Entity, để cho phép sử dụng các tính năng khóa ngoại của SQLite. No không những giúp thể hiện được tốt hơn mối quan hệ giữa các Entities, đảm bảo đúng thiết kế mà còn thực thi các ràng buộc trên các bảng để đảm bảo mối quan hệ hợp lệ khi bạn sửa đổi cơ sở dữ liệu.

Cụ thể chúng ta sẽ cùng đến một ví dụ cụ thể:

image

Cùng nhìn quan hệ 1-n giữa Person và Dog. Cụ thể với 2 Primary Key tương ứng là PersonIdDogId cùng với PersonId được sử dụng như là một foreign key.

@Entity(tableName = “dog”,
        foreignKeys = arrayOf(
            ForeignKey(entity = Person::class,
                       parentColumns = arrayOf(“personId),
                       childColumns = arrayOf("owner"))))

data class Dog(@PrimaryKey val dogId: String,
              val name: String,
              val owner: String)

Theo tùy chọn, bạn có thể tuỳ chọn hành động sẽ được thực hiện khi đối tượng Parent Entity bị xóa hoặc cập nhật trong cơ sở dữ liệu.

Bạn có thể chọn một trong các tùy chọn sau: NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT hoặc CASCADE, tương tự sử dụng như trong SQLite.

2. Tạo mối quan hệ Relation trong Room Database

Vẫn là mối quan hệ 1-n ở ví dụ trước. Bây giờ mình muốn truy vấn thực hiện việc lấy dữ liệu của các Person và toàn bộ Dogs tương ứng kèm theo.

image

Cách thông thường để thực hiện, chúng ta sẽ cần thực hiện 2 truy vấn: một truy vấn để lấy danh sách tất cả Person và một truy vấn khác để lấy danh sách Dog dựa trên Id của Person. Cụ thể:

@Query(“SELECT * FROM Person”)
public List<Person> getPersons();

@Query(“SELECT * FROM dog where owner = :personId”)
public List<Dog> getDogsForPersons(String personId);

Sau đây, mình sẽ triển khai theo cách thực hiện tạo mỗi quan hệ Relation giữa 2 đối tượng Person và Dog thông qua annotation @Relation

class PersonAndDogs {
   @Embedded
   var person: Person? = null
   @Relation(parentColumn = “personId”,
             entityColumn = “owner”)
   var dogs: List<Dog> = arrayListOf()
}

Trong DAO, chúng tôi chỉ thực hiện một truy vấn duy nhất và Rôm sẽ truy vấn cả bảng Person và Dog, tiếp đó xử lý mapping đối tượng.

@Transaction
@Query(“SELECT * FROM Person”)
List<PersonAndDogs> getPersonAnDogs();

3. Thực hiện câu lệnh trong một Transaction

Khi một câu lệnh trong Room gắn với @Transaction, nó sẽ đảm bảo rằng tất cả các hoạt động cơ sở dữ liệu mà bạn đang thực hiện trong phương thức đó sẽ được chạy bên trong một transaction.

Transaction sẽ thất bại khi một Exception trong một trong những truy vấn trong Transaction đó xảy ra.

@Dao
abstract class UserDao {
    
    @Transaction
    open fun updateData(users: List<User>) {
        deleteAllUsers()
        insertAll(users)
    }

    @Insert
    abstract fun insertAll(users: List<User>)

    @Query("DELETE FROM Users")
    abstract fun deleteAllUsers()
}

Bạn cũng có thể sử dụng @Transaction cho các phương thức @Query có câu lệnh chọn, trong các trường hợp sau:

  • Khi kết quả của truy vấn khá lớn. Bằng cách truy vấn cơ sở dữ liệu trong một giao dịch, bạn đảm bảo rằng nếu kết quả truy vấn không vừa với single cursor window, thì nó sẽ không bị hỏng do những thay đổi trong cơ sở dữ liệu giữa các lần cursor window swaps.

  • Khi kết quả của truy vấn là POJO với các trường @Relation. Các trường là các truy vấn riêng biệt nên việc chạy chúng trong một Transation sẽ đảm bảo kết quả nhất quán giữa các truy vấn.

4. Tối ưu hoá đối tượng truy vấn

Khi truy vấn cơ sở dữ liệu, bạn nên cân nhắc rằng có sử dụng tất cả các fields bạn trả về trong truy vấn của mình không?

Quan tâm đến dung lượng bộ nhớ mà ứng dụng của bạn sử dụng và chỉ tải tập hợp con các trường mà bạn sẽ sử dụng. Điều này cũng sẽ cải thiện tốc độ truy vấn của bạn bằng cách giảm IO cost.

Room sẽ thực hiện ánh xạ giữa các Columns và Objects cho bạn.

Cùng xem một ví dụ sau:

@Entity(tableName = "users")
data class User(@PrimaryKey
                val id: String,
                val userName: String,
                val firstName: String, 
                val lastName: String,
                val email: String,
                val dateOfBirth: Date, 
                val registrationDate: Date)

Trên một số trường hợp cụ thể, các bạn không cần hiển thị tất cả thông tin này. Vì vậy, thay vào đó, chúng ta có thể tạo một đối tượng UserMinimal chỉ chứa dữ liệu cần thiết.

data class UserMinimal(val userId: String,
                       val firstName: String, 
                       val lastName: String)

Trong lớp DAO, bạn chỉ cần xác định truy vấn như sau.

@Dao
interface UserDao {
    @Query(“SELECT userId, firstName, lastName FROM Users)
    fun getUsersMinimal(): List<UserMinimal>
}

5. Khởi tạo trước dữ liệu cho Room Database

Có rất nhiều trường hợp cụ thể mà bạn muốn thiết lập một bộ dữ liệu Default vào cơ sở dữ liệu trước. Khi đó bạn nên cân nhắc đến việc chèn dữ liệu ngay sau khi Room được khởi tạo.

Cụ thể, bạn có thể cân nhắc sử dụng RoomDatabase#Callback! Gọi phương thức addCallback khi xây dựng RoomDatabase của bạn và ghi đè onCreate hoặc onOpen.

  • onCreate will be called when the database is created for the first time, after the tables have been created.
  • onOpen is called when the database was opened.
   companion object {

        @Volatile private var INSTANCE: DataDatabase? = null

        fun getInstance(context: Context): DataDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        DataDatabase::class.java, "Test.db")
                        // prepopulate the database after onCreate was called
                        .addCallback(object : Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                super.onCreate(db)
                                // insert the data on the IO Thread
                                ioThread {
                                    getInstance(context).dataDao().insertData(PRE_POPULATE_DATA)
                                }
                            }
                        })
                        .build()

        val PRE_POPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
    }
private val IO_EXECUTOR = Executors.newSingleThreadExecutor()

/**
 * Utility method to run blocks on a dedicated background thread, used for io/database work.
 */
fun ioThread(f : () -> Unit) {
    IO_EXECUTOR.execute(f)
}

Note: Các bạn chú ý thực hiện việc Insert default data trên một Worker Thread.

Trên đây là một số Tips nhỏ trong việc sử dụng và triển khai Room Database. Mong là ít nhiều sẽ giúp ích cho mọi người. Cảm ơn mọi người đã theo dõi.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí