利用Room实现视频浏览历史本地记录

Room是什么?

Room是Google官方最近推出架构组件中的一个库,是Android平台上的ORM框架,它抽象封装了对SQlite数据库的操作,使得我们使用起来非常方便。

为什么我们要用Room?

Android平台上的ORM框架很多,比较常用的有Realm, LitePal, ObjectBox, greenDAO,Realm薄荷项目中有用到,有一些坑,LitePal性能比较差,greenDao性能较好,但使用起来比较麻烦。

Room的优势

  • Google的亲儿子,较其它库新
  • 使用注解,动态代理,类似于Retrofit的使用方式,使用成本低
  • 性能最优Realm, ObjectBox or Room. Which one is for you?
  • 支持RxJava,LiveData 可以方便实现数据绑定功能

怎么用?

我们现在来以一个本地记录视频浏览历史的需求为例学习Room的使用。
这里我们就不全面介绍Room了,它官方文档很全,详细用法可以去看官方文档

简单来说Room为我们提供了对数据库访问的简单操作,它主要有3个组件:

  • Database 是一个数据库实例的抽象,里面包含了数据操作层Dao的实例
  • Dao 是数据库对外暴露的操作接口,利用它来实现对数据库的增删改查操作
  • Entity 是具体的模型实体类,对应数据库中的表

如图:

首先我们添加对Room库的引用

在根项目(Project)的build.gradle中添加

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' } //添加此行
    }
}

在使用的module下的build.gradle中添加

implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

// For testing Room migrations, add:
testImplementation "android.arch.persistence.room:testing:1.0.0"

// For Room RxJava support, add:
implementation "android.arch.persistence.room:rxjava2:1.0.0"

这里有一个小坑,我的项目中使用Room的是DataLayer层,是一个非Android的项目,所以在该module下的gradle中添加

annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

没什么卵用,需要将其再添加一份到App module下面 同时不能删掉DataLayer下gradle中的依赖,只添加在app module下面会在运行时报错(没有在编译时生成Database的实现类导致运行时操作数据库找不到该类异常),只添加在Data module下会编译时报错(找不到Room中注解的类).

需求分析

实现视频浏览历史记录本地记录有两个动作:

  • 向数据库插入一条记录 需要保证相同视频且相同用户下的记录有且只有一条,如果在插入一条记录前发现数据库中有相同视频且相同用户的一条记录,这时就需要覆盖旧的记录。
  • 查询 有一个观看记录列表的页面,所以这里需要实现一个以用户id为key的查询动作。

设计表结构,操作接口

根据页面展示需求,观看记录类结构如下:

@Entity(tableName = "watch_history_record",
        primaryKeys = {"video_id", "user_id"})
public class WatchHistoryRecord {
    @NonNull
    @ColumnInfo(name = "video_id")
    public long videoId;

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

    @ColumnInfo(name = "seek_position")
    public long position;

    @ColumnInfo(name = "action_url")
    public String actionUrl;

    @ColumnInfo(name = "play_url")
    public String playUrl;

    ...

    @ColumnInfo(name = "update_time")
    public long updateTime;
}

Entity 注解标识此类是数据库中表对应的类
tableName 是对应该表的名称,如果不写,默认是类名
primaryKeys 对应修饰主键,我们这里由于有上面需求的第一条,所以设计成 user_id 和 video_id 成一对组合主键
ColumnInfo 对应该表中该字段对应的列名,如果不写默认是字段名
由于防止以后修改类名和字段名导致的问题,这里将表名和列明都采用注解的方式声明.

数据库操作接口类结构如下:

@Dao
public interface WatchHistoryRecordDao {

    /**
    * @param userId the user id
    * @return the WatchHistoryRecord select from the table
    */
    @Query("SELECT * FROM watch_history_record where user_id == :userId ORDER BY update_time DESC")
    Flowable<List<WatchHistoryRecord>> list(String userId);

    /**
    * Insert a record in the database. If the record already exists, replace it.
    *
    * @param record the record to be inserted
    */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insert(WatchHistoryRecord record);

    @Query("DELETE FROM watch_history_record")
    void deleteAll();
}

提供三个操作,查询,插入,删除。

@Dao 标识该类为数据库访问接口 ,Room以此注解在编译时来生成具体的实现类.
@Query 标识一个Sql语句 这里我们需要传入一个参数userId 采用 “: 参数名” 的形式传入.
@Insert 标识一个插入操作,onConflict标识数据库操作事务的策略,此处为新值替换旧值的策略.

数据库类:

@Database(entities = {WatchHistoryRecord.class}, version = 1, exportSchema = false)
public abstract class EyepetizerDatabase extends RoomDatabase {

    private static volatile EyepetizerDatabase INSTANCE;

    public abstract WatchHistoryRecordDao watchHistoryRecordDao();

    public static EyepetizerDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (EyepetizerDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            EyepetizerDatabase.class, "Eyepetizer.db")
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

@Database 标识此类是一个Room的Database类.
@entities 标识所包含的表对应的模型类名,version对应版本名.

Database具体的实现类由编译时注解框架生成,其中包含获取数据库操作接口Dao类的方法,所以我们在外部就通过Database的实例方法调用完成对数据库的操作,如下:

@Override
public Flowable<List<WatchRecord>> listWatchRecord(String userId) {
    return EyepetizerDatabase.getInstance(mContext)
            .watchHistoryRecordDao()
            .list(userId)
            .map(watchHistoryRecords -> {
                List<WatchRecord> watchRecords = new ArrayList<>();
                for (WatchHistoryRecord record : watchHistoryRecords) {
                    watchRecords.add(WatchRecordMapper.from(record));
                }
                return watchRecords;
            });
}

@Override
public Completable insert(WatchRecord watchRecord) {
    return Completable.create(e -> {
        EyepetizerDatabase.getInstance(mContext)
                .watchHistoryRecordDao()
                .insert(WatchRecordMapper.to(watchRecord));
        e.onComplete();
    });
}

ok, 到这里对 Room 的简单学习使用就结束了,剩下的就是页面UI代码了。本例完整代码传送门
这里需要注意的是Room中对数据库的操作必须在另外的线程进行(毕竟是io耗时操作,但Room强制要求这么做,如果在主线程调用,直接就会导致崩溃)

最后

谢谢阅读!