Context

Trong xu hướng lập trình mobile hiện đại, càng lúc người ta càng chú trọng đến trải nghiệm người dùng (UX). Một ứng dụng tốt không chỉ là một ứng dụng đầy đủ chức năng, làm được những thứ “vi diệu” mà còn phải đáp ứng được trải nghiệm tốt cho người dùng. Để làm được điều đó, lập trình viên có tư duy cần phải có tư duy làm sao mang đến sự thoải mái nhất có thể cho các người dùng úng dụng của mình, đơn cử như việc hỗ trợ offline. Rất nhiều ứng dụng hiện nay cho phép sử dụng offline, như facebook có chức năng xem newfeed offline, comment, post ảnh, status offline, Google map cho phép xem bản đồ khi không có mạng, Instagram cho phép post thậm chí là xem ảnh, video offline … Rất nhiều app với xu hướng hỗ trợ việc xử lý khi không có kết nối mạng.

Có rất nhiều loại database dành cho mobile mới xuất hiện gần đây, ưu có, nhược có nhưng về độ ổn định, SQLite là hàng đầu. Tuy nhiên, làm việc với nó theo cách thông thường mà lâu nay chúng ta biết thực sự tốn khá nhiều thời gian cho việc code lẫn maintain. Room Persistence Library – một lớp trừu tượng cover SQLite được giới thiệu và recommend bởi các kỹ sư Google là một lựa chọn không tồi. Với khuôn khổ bài viết này, chúng ta sẽ tìm hiểu một cách tổng quan về Room nhé.

What ‘s Room ?

Room đơn giản là một thư viện để tạo ra một abstraction layer phủ lên SQLite. Nói cách khác, nó giống như một lớp trung gian, với đầy đủ các method, để bạn thao tác với SQLite được dễ dàng và hiệu quả hơn. Và dĩ nhiên, Room không phải là một Database Management nhé.

Có 3 thành phần chính trong Room

  • Database : Được annotate bởi @Database , tạo một điểm truy cập chính đến CSDL của ứng dụng. Khai báo bởi một abstract class và reference tới các Entity của Database.
  • Entity: Chính là các Model đại diện cho các table trong database
  • DAO : Là Implementation của DAO pattern, chứa các method làm việc với database.

Create Entities

Entity – đại diện cho table trong cơ sở dữ liệu – được định nghĩa đầy đủ các phương thức để làm việc với một CSDL quan hệ (Relational Database) bao gồn key, relationship, transaction …

Tạo một Entity

@Entity(tableName = "User")
public class User {
    
    @PrimaryKey
    private String uid;
    
    private String name;
    
    private String email;
    
    @ColumnInfo(name = "date_of_birth")
    private Date dob;
    
    private int weight;
    
    private int height;
    
    @Ignore
    private int ignore;
    
}
  • @PrimaryKey : khai báo field là khóa chính của table.
  • @ColumnInfo: Khai báo thuộc tính cho column
  • @Ignore : Bỏ qua trường này trong database

Đánh index

@Entity(tableName = "User", indices = { @Index(value = "uid") })
public class RoomUser {

    @PrimaryKey
    private String uid;

    ...
}

Thiết lập khai báo một khóa ngoại

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "uid",
                                  childColumns = "user_id"))
class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "user_id")
    public String userId;
}

Create DAOs

Nếu các bạn chưa tìm hiểu về DAO pattern, hãy xem bài viết trước đây của tôi nhé.
Chẳng có gì khác biệt cả, Room yêu cầu bạn tạo ra các interface hoặc abstract class và annotate nó bởi @Dao. Trong DAOs, bạn define các method để làm việc với database và Room sẽ gen ra các implementation trong quá trình build dựa vào các annotation tương ứng ở mỗi method.

@Dao
public abstract class UserDao {

    @Query("SELECT * FROM User WHERE uid = :uid")
    public abstract User get(String uid);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public abstract void save(User user);

    @Update
    public abstract void update(User user);

    @Delete
    public abstract void delete(User user);

}

Các method CRUD đều được cung cấp bởi Room để bạn làm việc với SQLite hiệu quả nhất.

Room tự động tạo các transaction khi bạn thao tác với database. Mỗi câu lệnh SQL đều được thực thi trong một transaction. Với ví dụ trên, khi save một User mới, Room sẽ tự động tạo 1 transaction, tương tự với update hay delete.

Room cũng cung cấp một annotation @Transaction khi bạn muốn thực hiện nhiều công việc trong cùng 1 transaction mà thôi. Ví dụ với đoạn mã dưới:

@Transaction
public void replace(User user){
    delete(user);
    save(user);
}

Combine with RxJava 2

Room support Reactive Programming, tuy nhiên mặc định là RxJava 2. Rất đơn giản

@Query("SELECT * FROM User WHERE uid = :uid")
public abstract Flowable<User> get(String uid);

Hay

@Query("SELECT * FROM User WHERE uid = :uid")
public abstract Maybe<User> get(String uid);

Dĩ nhiên, với ReactiveX, bạn hoàn toàn làm được rất nhiều điều hơn nữa và biến câu query của mình trở nên hay ho hơn rất nhiều. Chi tiết có thể tham khảo tại đây

Work with RxJava 1 ?

Room chỉ hỗ trợ mặc định với RxJava 2, vậy với các dự án đã lỡ theo RxJava 1 thì không thể apply Room? Câu trả lời là không đúng. Bạn hoàn toàn có thể apply Room combine with RxJava 1 vào dự án của mình nhé.

Tạo một Room API

public interface RoomApi {

    UserDao userDao();

    BookDao bookDao();

    <T> Observable<T> execute(Action2<Subscriber<? super T>, RoomApi> action);

    <T> Observable<T> query(Action2<Subscriber<? super T>, RoomApi> action);

}

Một implementation

public class RoomApiImpl implements RoomApi {

    private RoomDatabaseManager databaseManager;

    public RoomApiImpl(RoomDatabaseManager databaseManager) {
        this.databaseManager = databaseManager;
    }

    @Override
    public UserDao userDao() {
        return databaseManager.userDao();
    }

    @Override
    public BookDao bookDao() {
        return databaseManager.bookDao();
    }

    public <T> Observable<T> execute(final Action2<Subscriber<? super T>, RoomApi> action) {
        return Observable.create(new Observable.OnSubscribe<T>() {
            @Override
            public void call(Subscriber<? super T> subscriber) {
                try {
                    action.call(subscriber, RoomApiImpl.this);
                    subscriber.onCompleted();
                } catch (Exception ex) {
                    subscriber.onError(ex);
                }
            }
        }).subscribeOn(Schedulers.io());
    }

    @Override
    public <T> Observable<T> query(Action2<Subscriber<? super T>, RoomApi> action) {
        return execute(action).observeOn(AndroidSchedulers.mainThread());
    }
}

Class Database

@Database(entities = {
        User.class, Book.class
}, version = 1, exportSchema = false)
public abstract class RoomDatabaseManager extends RoomDatabase {

    public static final String DATABASE_NAME = "my_db";

    public abstract UserDao userDao();

    public abstract BookDao bookDao();
}

LocalDataSource hoặc Repository tôi sẽ thực hiện query như sau

public Observable<User> getUser(final String uid){
        return roomApi.query(new Action2<Subscriber<? super User>, RoomApi>() {
            @Override
            public void call(Subscriber<? super User> subscriber, RoomApi roomApi) {
                subscriber.onNext(roomApi.userDao().get(uid));
            }
        });
    }
    
public Observable<User> save(final User user) {
        return roomApi.execute(new Action2<Subscriber<? super User>, RoomApi>() {
            @Override
            public void call(Subscriber<? super User> subscriber, RoomApi roomApi) {
                subscriber.onNext(roomApi.userDao().save(user));
            }
        });
    }

Conclusion

Room là một thư viện làm việc với SQLite rất hay và được khuyến nghị. Trong khuôn khổ bài viết này, tôi chỉ giới thiệu đến các bạn ở mức cơ bản và dễ hiểu nhất. Nếu được, hãy tìm các tutorial chi tiết hơn và thử các sample về Room trên Github và xây dựng cho mình một ứng dụng nhỏ nhé.