Tình huống
Bạn vừa thêm cột profile_picture_url vào entity User của mình. Bạn nhấn 'Run', mong đợi tính năng mới hoạt động, nhưng ứng dụng bị crash ngay lập tức. Thay vì giao diện người dùng, bạn thấy lỗi java.lang.IllegalStateException trong Logcat. Điều này xảy ra do cấu trúc cơ sở dữ liệu trong mã nguồn không còn khớp với tệp được lưu trên thiết bị.
java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.
Tại sao lỗi này xảy ra
Room đóng vai trò như người gác cổng cho dữ liệu cục bộ của bạn. Mỗi khi bạn biên dịch dự án, Room sẽ tạo ra một identity hash duy nhất. Hãy coi đây là dấu vân tay 32 ký tự của toàn bộ schema—bao gồm các bảng, cột và ràng buộc. Room lưu mã hash này trong một bảng ẩn tên là room_master_table bên trong tệp SQLite.
Khi ứng dụng khởi chạy, Room so sánh dấu vân tay của mã nguồn hiện tại với dấu vân tay được lưu trên ổ đĩa. Nếu chúng khác nhau và số version vẫn giữ nguyên, Room sẽ báo lỗi. Nó dừng ứng dụng để ngăn chặn việc làm hỏng dữ liệu, vì nó không biết cách chuyển dữ liệu cũ sang cấu trúc mới.
Cách khắc phục nhanh (Trong giai đoạn thử nghiệm)
Nếu bạn vẫn đang ở giai đoạn đầu của quá trình phát triển và không phải lo lắng về dữ liệu người dùng thực tế, bạn có thể khắc phục lỗi này trong vài giây bằng cách xóa sạch dữ liệu cũ.
- Xóa dữ liệu ứng dụng: Mở Settings > Apps > [Ứng dụng của bạn] > Storage và nhấn "Clear Data."
- Cài đặt lại: Xóa ứng dụng khỏi trình giả lập hoặc điện thoại và chạy lại từ Android Studio.
Thao tác này sẽ xóa hoàn toàn tệp .db. Trong lần khởi chạy tiếp theo, Room sẽ tạo một cơ sở dữ liệu mới bằng schema mới và lưu mã hash đã cập nhật. Cách này ổn khi phát triển (dev), nhưng đừng bao giờ thực hiện trong môi trường production.
Cách khắc phục lâu dài cho môi trường Production
Khi ứng dụng của bạn đã phát hành và có hàng nghìn người dùng, bạn không thể yêu cầu họ xóa dữ liệu. Bạn có hai cách đáng tin cậy để xử lý thay đổi này.
Tùy chọn 1: Destructive Migration (Mất dữ liệu)
Sử dụng cách này nếu cơ sở dữ liệu của bạn chỉ là bộ nhớ đệm (cache) và việc mất dữ liệu không ảnh hưởng đến trải nghiệm người dùng. Đầu tiên, hãy tăng số phiên bản từ 1 lên 2 (hoặc bất kỳ phiên bản hiện tại nào của bạn).
@Database(entities = [User::class], version = 2)
abstract class AppDatabase : RoomDatabase() { ... }
Tiếp theo, yêu cầu Room builder sử dụng fallbackToDestructiveMigration():
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "app-db"
)
.fallbackToDestructiveMigration()
.build()
Room sẽ phát hiện sự thay đổi phiên bản, xóa các bảng cũ và tạo lại chúng. Không còn crash, nhưng cơ sở dữ liệu sẽ trống rỗng lúc bắt đầu.
Tùy chọn 2: Manual Migration (Bảo toàn dữ liệu)
Đây là tiêu chuẩn chuyên nghiệp. Bạn phải viết một đoạn script hướng dẫn SQLite cách sửa đổi các bảng hiện có một cách chính xác. Giả sử bạn đã thêm cột age vào bảng User.
Bước 1: Tăng phiên bản trong class @Database.
@Database(entities = [User::class], version = 2)
abstract class AppDatabase : RoomDatabase() { ... }
Bước 2: Tạo một đối tượng migration. Mã này chạy một lệnh SQL cụ thể để cập nhật cấu trúc bảng mà không chạm vào các hàng hiện có.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Bước 3: Đăng ký migration trong builder của bạn.
Room.databaseBuilder(context, AppDatabase::class.java, "app-db")
.addMigrations(MIGRATION_1_2)
.build()
Xác minh và Gỡ lỗi
Làm thế nào để biết migration thực sự hoạt động? Hãy sử dụng Database Inspector trong Android Studio (View > Tool Windows > App Inspection). Bạn có thể xem cấu trúc bảng trực tiếp và chạy các truy vấn SQL để xác minh các cột mới đã tồn tại.
Nếu bạn đang xử lý một schema phức tạp và không thể tìm ra lý do tại sao các mã hash không khớp, hãy kiểm tra các tệp schema đã xuất. Đặt exportSchema = true trong annotation database của bạn. Điều này sẽ tạo ra một tệp JSON cho mỗi phiên bản. Đôi khi, ngay cả một chi tiết nhỏ như sự khác biệt về giá trị DEFAULT cũng có thể gây ra sự không khớp. Tôi thường sử dụng Hash Generator tại ToolCraft để so sánh mã hash MD5 của các tệp JSON này. Nó giúp xác định xem một ký tự ẩn hoặc thay đổi định dạng có khiến Room từ chối schema hay không.
Mẹo nhỏ: Luôn cập nhật số phiên bản ngay khi bạn thay đổi một @Entity. Nó sẽ giúp bạn tránh khỏi việc phải truy tìm các lỗi crash bí ẩn sau đó.

