+3

Thay Thế Database Của Ứng Dụng Bằng Room Database

Room Database(Room Persistence Library) là một phần trong Android Architecture Components, nó giúp cho việc thao tác với SQLiteDatabase Object trở lên dễ dàng hơn, giảm thiểu số lượng câu truy vấn bằng những annotation sẵn có và xác minh truy vấn tại thời điểm biên dịch.

Đó chỉ là một phần của trải nghiệm và còn nhiều thú vị hơn khi chúng ta đi sâu vào tìm hiểu thông qua ví dụ thực tế. Chắc hẳn đã có bạn đọc bài viết của mình về Architecture ViewModel trong đó có một ví dụ thực tiễn và hôm nay mình sẽ ứng dụng trực tiếp Room để cải tiến project này.

1. Cấu tạo của Room Database

Điều đầu tiên chúng ta sẽ quan tâm đấy là thành phần Room database gồm những gì.

  • SQLite database: Đây chính là thành phần cốt lõi mà các data được lưu trữ trong đây. Và file này được lưu trong thiết bị.
  • DAO (Data access object): Chứa các ánh xạ SQLite tới các hàm CRUD. Chắc hẳn bạn vẫn còn nhớ mình đã phải rất cẩn thận viết câu queries SQLite chứ, nếu chẳng may có sai thì phải dò lại rất cẩn thận. Nhưng bây giờ điều đó trở lên đơn giản hơn rất nhiều, khi bạn dùng DAO và call các method rồi Room sẽ làm tiếp những phần còn lại. (Cách queries cũ mình để phía dưới để lát nữa bạn tiện so sánh). Tiếp nhé ^^
  • Entity: Đây chính là những đối tượng nhỏ bạn cần lưu trữ (table) Ex: user table, event table,... Hiện tại tất cả việc định nghĩa thuộc tính đã được support bằng annotations rất ngắn gọn.
  • Room database: Đây là lớp abstract layer nằm ngoài cùng đóng vai trò quản lý SQLite database và cho phép truy cập tới tầng dưới cùng của SQLite. Room dùng DAO để thực hiện các queries tới database.

(Cách queries cũ với SQLiteOpenHelper)

EmployeeOperations.java

public class EmployeeOperations {
    public static final String LOGTAG = "EMP_MNGMNT_SYS";
 
    SQLiteOpenHelper dbhandler;
    SQLiteDatabase database;
 
    private static final String[] allColumns = {
            EmployeeDBHandler.COLUMN_ID,
            EmployeeDBHandler.COLUMN_FIRST_NAME,
            EmployeeDBHandler.COLUMN_LAST_NAME,
            EmployeeDBHandler.COLUMN_GENDER,
            EmployeeDBHandler.COLUMN_HIRE_DATE,
            EmployeeDBHandler.COLUMN_DEPT
 
    };
 
    public EmployeeOperations(Context context){
        dbhandler = new EmployeeDBHandler(context);
    }
 
    public void open(){
        Log.i(LOGTAG,"Database Opened");
        database = dbhandler.getWritableDatabase();
 
 
    }
    public void close(){
        Log.i(LOGTAG, "Database Closed");
        dbhandler.close();
 
    }
    public Employee addEmployee(Employee Employee){
        ContentValues values  = new ContentValues();
        values.put(EmployeeDBHandler.COLUMN_FIRST_NAME,Employee.getFirstname());
        values.put(EmployeeDBHandler.COLUMN_LAST_NAME,Employee.getLastname());
        values.put(EmployeeDBHandler.COLUMN_GENDER, Employee.getGender());
        values.put(EmployeeDBHandler.COLUMN_HIRE_DATE, Employee.getHiredate());
        values.put(EmployeeDBHandler.COLUMN_DEPT, Employee.getDept());
        long insertid = database.insert(EmployeeDBHandler.TABLE_EMPLOYEES,null,values);
        Employee.setEmpId(insertid);
        return Employee;
 
    }
 
    // Getting single Employee
    public Employee getEmployee(long id) {
 
        Cursor cursor = database.query(EmployeeDBHandler.TABLE_EMPLOYEES,allColumns,EmployeeDBHandler.COLUMN_ID + "=?",new String[]{String.valueOf(id)},null,null, null, null);
        if (cursor != null)
            cursor.moveToFirst();
 
        Employee e = new Employee(Long.parseLong(cursor.getString(0)),cursor.getString(1),cursor.getString(2),cursor.getString(3),cursor.getString(4),cursor.getString(5));
        // return Employee
        return e;
    }
 
    public List<Employee> getAllEmployees() {
 
        Cursor cursor = database.query(EmployeeDBHandler.TABLE_EMPLOYEES,allColumns,null,null,null, null, null);
 
            List<Employee> employees = new ArrayList<>();
        if(cursor.getCount() > 0){
            while(cursor.moveToNext()){
                Employee employee = new Employee();
                employee.setEmpId(cursor.getLong(cursor.getColumnIndex(EmployeeDBHandler.COLUMN_ID)));
                employee.setFirstname(cursor.getString(cursor.getColumnIndex(EmployeeDBHandler.COLUMN_FIRST_NAME)));
                employee.setLastname(cursor.getString(cursor.getColumnIndex(EmployeeDBHandler.COLUMN_LAST_NAME)));
                employee.setGender(cursor.getString(cursor.getColumnIndex(EmployeeDBHandler.COLUMN_GENDER)));
                employee.setHiredate(cursor.getString(cursor.getColumnIndex(EmployeeDBHandler.COLUMN_HIRE_DATE)));
                employee.setDept(cursor.getString(cursor.getColumnIndex(EmployeeDBHandler.COLUMN_DEPT)));
                employees.add(employee);
            }
        }
        // return All Employees
        return employees;
    }
 
 
 
 
    // Updating Employee
    public int updateEmployee(Employee employee) {
 
        ContentValues values = new ContentValues();
        values.put(EmployeeDBHandler.COLUMN_FIRST_NAME, employee.getFirstname());
        values.put(EmployeeDBHandler.COLUMN_LAST_NAME, employee.getLastname());
        values.put(EmployeeDBHandler.COLUMN_GENDER, employee.getGender());
        values.put(EmployeeDBHandler.COLUMN_HIRE_DATE, employee.getHiredate());
        values.put(EmployeeDBHandler.COLUMN_DEPT, employee.getDept());
 
        // updating row
        return database.update(EmployeeDBHandler.TABLE_EMPLOYEES, values,
                EmployeeDBHandler.COLUMN_ID + "=?",new String[] { String.valueOf(employee.getEmpId())});
    }
 
    // Deleting Employee
    public void removeEmployee(Employee employee) {
 
        database.delete(EmployeeDBHandler.TABLE_EMPLOYEES, EmployeeDBHandler.COLUMN_ID + "=" + employee.getEmpId(), null);
    }
 
 
 
}

2. Bước cài đặt

Sau khi am hiểu hoặc mới chỉ nắm bắt được các thành phần của Room rồi chúng ta sẽ cùng nhau implement vào project nhé. Đầu tiên phải thêm Google Maven repository, nếu bạn chưa có thì vào build.gradle :

allprojects {
    repositories {
        jcenter()
        google()
    }
}

Thêm dependencies vào app/build.gradle vì trong project chúng ta dùng thêm Lifecycle để thực thi việc thay đổi data:

    //Room
    implementation "android.arch.persistence.room:runtime:$rootProject.room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$rootProject.room_version"

    //Lifecycle
    implementation "android.arch.lifecycle:extensions:$rootProject.lifecycle_version"
    annotationProcessor "android.arch.lifecycle:compiler:$rootProject.lifecycle_version"

Bước cài đặt khá nhanh và đơn giản, tiếp theo chúng ta chuyển bước số 3

3. Khởi tạo các thành phần của Room

Vừa rồi chúng ta thấy có 4 thành phần nhưng rõ ràng SQLite là nhân và bạn không cần khởi tạo nên bây giờ lần lượt sẽ tiến hành tạo : Entity, DAO và RoomDatabase.

Entity

Chúng ta thay đổi User.class -> UserEntity như sau:

Chú ý: Để hiểu sâu về điều này các bạn vui lòng fork project của mình và checkout sang branch develop : GitHub Source

Đây là code lúc chưa implement Room để tiện so sánh việc thay đổi như nào. Ở đây mình sẽ đăng tải code đã chỉnh sửa để phù hợp với nội dung bài viết.

UserEntity.java

@Entity(tableName = "user_table")
public class UserEntity implements User {
    @PrimaryKey(autoGenerate = true)
    private int id;
    @ColumnInfo(name = "name")
    @NonNull
    private String name;
    @ColumnInfo(name = "job")
    @NonNull
    private String job;
    @ColumnInfo(name = "idImage")
    @NonNull
    private int idImage;

    public UserEntity(String name, String job, int idImage) {
        this.name = name;
        this.job = job;
        this.idImage = idImage;
    }

    @Override
    public int getId() {
        return id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getJob() {
        return job;
    }

    @Override
    public int getIdImage() {
        return idImage;
    }

    public void setId(int id) {
        this.id = id;
    }
}

DAO:

Với mỗi Entity các bạn tạo 1 class DAO bởi vì đây là class thực hiện việc queries đến SQLite và rõ ràng table khác nhau thì việc xử lý phải riêng biệt. Đến đây nếu còn chưa hiểu bạn có thể đọc lại nội dung ở trên hoặc comment phía dưới bài viết nha.

UserDao.java

@Dao
public interface UserDao {
    @Query("SELECT * FROM user_table ORDER BY name ASC")
    LiveData<List<UserEntity>> loadAllUsers();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAll(List<UserEntity> users);

    @Query("select * from user_table where id = :userId")
    LiveData<UserEntity> loadUser(int userId);

    @Query("DELETE FROM user_table WHERE id = :userId")
    void deleteUser(int userId);
}

Chú ý: Các bạn đừng quên annotation @Dao nha

Rất tốt bây giờ chúng ta sắp xong rồi, thêm 1 class Room database để quản lý tất cả Entity

Room Database:

UserRoomDB.java

@Database(entities = { UserEntity.class }, version = 1)
public abstract class UserRoomDB extends RoomDatabase {

    public abstract UserDao mUserDao();

    public static final String DATABASE_NAME = "user-list-archvm-db";
    private static UserRoomDB sInstance;
    private final MutableLiveData<Boolean> mIsDatabaseCreated = new MutableLiveData<>();

    public static UserRoomDB getInstance(final Context context, final AppExecutors executors) {
        if (sInstance == null) {
            synchronized (UserRoomDB.class) {
                if (sInstance == null) {
                    sInstance = buildDatabase(context.getApplicationContext(), executors);
                    sInstance.updateDatabaseCreated(context.getApplicationContext());
                }
            }
        }
        return sInstance;
    }

    private static UserRoomDB buildDatabase(final Context appContext, final AppExecutors executors) {
        return Room.databaseBuilder(appContext, UserRoomDB.class, DATABASE_NAME)
                .addCallback(new Callback() {
                    @Override
                    public void onCreate(@NonNull SupportSQLiteDatabase db) {
                        super.onCreate(db);
                        executors.diskIO().execute(new Runnable() {
                            @Override
                            public void run() {
                                UserRoomDB database = UserRoomDB.getInstance(appContext, executors);
                                List<UserEntity> userEntities = DataGenerator.generateUsers();
                                insertData(database, userEntities);
                                database.setDatabaseCreated();
                            }
                        });
                    }
                })
                .build();
    }

    private static void insertData(final UserRoomDB database, final List<UserEntity> userEntities) {
        database.runInTransaction(new Runnable() {
            @Override
            public void run() {
                database.mUserDao().insertAll(userEntities);
            }
        });
    }

    private void updateDatabaseCreated(final Context context) {
        if (context.getDatabasePath(DATABASE_NAME).exists()) {
            setDatabaseCreated();
        }
    }

    private void setDatabaseCreated() {
        mIsDatabaseCreated.postValue(true);
    }

    public LiveData<Boolean> getDatabaseCreated() {
        return mIsDatabaseCreated;
    }
}

Ở đây chúng ta đã khởi tạo những record ban đầu vào trong table User, vì vậy mà trong Callback của function create DB đã insert data trong đó.

Trong bài viết trước chúng ta đã dùng Architecture ViewModel, do vậy cần có một config để lắng nghe data thay đổi thì cần update lại View bằng việc sử dụng LiveData. Đây cũng chính là lý do chúng ta cần import thêm Lifecycle đã nói ở trên.

Thay đổi ở ViewModel:

UserViewModel.java

public class UserViewModel extends AndroidViewModel {
    private final MediatorLiveData<List<UserEntity>> mObservableUsers;

    public UserViewModel(@NonNull Application application) {
        super(application);
        mObservableUsers = new MediatorLiveData<>();
        mObservableUsers.setValue(null);

        LiveData<List<UserEntity>> products = BaseApp.getInstance().getRepository().getUserEntity();

        // observe the changes of the products from the database and forward them
        mObservableUsers.addSource(products, new Observer<List<UserEntity>>() {
            @Override
            public void onChanged(@Nullable List<UserEntity> userEntities) {
                mObservableUsers.setValue(userEntities);
            }
        });
    }

    public LiveData<List<UserEntity>> getUserList() {
        return mObservableUsers;
    }
}

MainActivity.java

private void subscribeUi(UserViewModel viewModel) {
        // Update the list when the data changes
        viewModel.getUserList().observe(this, new Observer<List<UserEntity>>() {
            @Override
            public void onChanged(@Nullable List<UserEntity> userEntities) {
                if (userEntities != null) {
//                    mBinding.setIsLoading(false);
                    mAdapter.setUserList(userEntities);
                }
                // espresso does not know how to wait for data binding's loop so we execute changes
                // sync.
                mBinding.executePendingBindings();
            }
        });
    }

Như vậy các bước chúng ta đều hoàn thiện, các bạn hay chạy thử project của mình để kiểm tra kết quả và so sánh với source code cũ để hiểu thêm cách phối hợp giữa các Architecture Components với nhau.

4. Tổng kết

Vừa rồi mình đã cung cấp cách áp dụng Room database để thay thế cho database hiện tại, ngoài việc mong muốn chúng ta đều có cách tiếp cận công nghệ mới và đồng thời giúp cho việc phát triển Android APP chất lượng hơn. Hy vọng rằng với những nội dung trên đây sẽ hữu ích cho các bạn!!!

Tất cả source code mình đã để ở đây GitHub Source


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.