Jetpack组件之Room

Jetpack组件之Room

Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager

首语

Android使用SQLite作为数据库存储数据,但是SQLite使用繁琐且容易出错,有许多开源的数据如GreenDAO、ORMLite等,这些都是为了方便SQLite的使用而出现的,Google也意识到了这个问题,在Jetpack组件中推出了Room,Room在SQLite上提供了一层封装,可以流畅的访问数据库。

优势

  1. 拥有SQLite的所有操作功能。
  2. 使用简单,通过注解的方式实现相关功能,编译时自动生成实现类impl。
  3. 与LiveData、LifeCycle及Paging天然支持。

依赖

  implementation "androidx.room:room-runtime:2.2.6"
  annotationProcessor "androidx.room:room-compiler:2.2.6"

相关概念

Room主要包含三个组件:

  • 数据库:包含数据库持有者,作为应用已保留的持久关系型数据的底层连接的主要接入点。
    使用@Database注解的类应满足以下条件:
    • 是扩展RoomDatabase的抽象类。
    • 在注释中添加与数据库关联的实体列表。
    • 包含具有0个参数且返回使用@Dao注释的类的抽象方法。
  • Entity:表示数据库中的表。
  • DAO:包含用于访问数据库的方法。

应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。 最后,应用使用实体来获取和设置与数据库中的表列相对应的值。Room架构图如图所示。
Room架构图

使用

创建数据库。

//exportSchema = true 生成数据库创建表或升级等操作及字段描述的json文件
//修改数据库版本直接通过version修改
//SkipQueryVerification注解是编译时候是否验证SQL语句正确与否
@SkipQueryVerification
@Database(entities = {Student.class}, version = 1, exportSchema = false)
//数据读取、存储时数据转换器,比如将写入时将Date转换成Long存储,读取时把Long转换Date返回
//public class DateConverter {
//    @TypeConverter
//    public static Long date2Long(Date date) {
//        return date.getTime();
//    }
//
//    @TypeConverter
//    public static Date long2Date(Long data) {
//        return new Date(data);
//    }
//}
//@TypeConverters(DateConverter.class)
//写成抽象类可以不实现默认方法,编译时候会生成一个DataBase实现类
public abstract class StudentDatabase extends RoomDatabase {

    private static volatile StudentDatabase database;

    private StudentDatabase() {
    }

    public static StudentDatabase getInstance() {
        if (database == null) {
            synchronized (StudentDatabase.class) {
                if (database == null) {
                    //创建一个内存数据库
                    //但是这种数据库的数据只存在于内存中,也就是进程被杀之后,数据随之丢失
                    //Room.inMemoryDatabaseBuilder()
                    database = Room.databaseBuilder(AppGlobals.getApplication(), StudentDatabase.class, "room_cache")
                            //是否允许在主线程进行查询
                            .allowMainThreadQueries()
                            //数据库创建和打开后的回调
                            //.addCallback()
                            //设置查询的线程池
                            //.setQueryExecutor()
                            //设置数据工厂,默认FrameworkSQLiteOpenHelperFactory
                            //.openHelperFactory()
                            //room的日志模式,默认AUTOMATIC
                            //.setJournalMode()
                            //数据库升级异常之后的回滚,但是数据表被重新创建,数据也会丢失
                            .fallbackToDestructiveMigration()
                            //数据库升级异常后根据指定版本进行回滚
                            .fallbackToDestructiveMigrationFrom()
                            /*
                             * 数据库升级,须谨慎,
                             * 如果用户数据库版本是1,需要直接升级到版本3,Room会判断有没有从1到3的升级方案,如果没有,则按照从1到2,再到3,
                             * 可以添加多个升级方案
                             */
                            .addMigrations(StudentDatabase.sMigration, StudentDatabase.mMigration)
                            .build();
                }
            }
        }
        return database;
    }

    static Migration sMigration = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("alter table teacher rename to student");
            database.execSQL("alter table teacher add column teacher_age INTEGER NOT NULL default 0");
        }

    };

    static Migration mMigration = new Migration(2, 3) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            //升级操作
        }
    };
}

注意:如果我们设置了exportSchema(默认值是true),需要在app.gradle中配置存放位置。

defaultConfig {
		...
        //配置room生成的json文件位置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments=["room.schemaLocation":"$projectDir/schemas".toString()]
            }
        }
    }

关于Room的诸多注解,可参考Room的源码,在room_common jar包下,注释非常详细。

创建Entity

@Fts4(languageId ="china")
//foreignKeys 外键, user表中的key和Student表中的id相互关联,parentColumns="User表列名",childColumns="当前表列名",onDelete时 NO_ACTION(默认,不操作);RESTRICT(相关联);SET_NULL(设置为Null);SET_DEFAULT(设置为默认值);CASCADE(删除或更新相关联)
@Entity(tableName = "student" ,foreignKeys = {@ForeignKey(entity = User.class,parentColumns = "id",childColumns = "key",onDelete = ForeignKey.CASCADE,onUpdate = ForeignKey.RESTRICT)}, ignoredColumns = "score",indices = {@Index("index"),@Index(value = {"name","age"},unique = true)})
public class Student extends Score{

    //PrimaryKey 主键,必须要有,且不为空,autoGenerate 主键的值是否由Room自动生成,默认false
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    public int id;
    
    //@ColumnInfo(name = "name"),指定该字段在表中的列的名字;typeAffinity指定数据类型
    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    public String name;

    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
    public String age;

    /**
     * Room默认使用该构造器
     */
    public Student(int id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    /**
     * Room只能识别一个构造器,如果希望定义多个构造器
     * 可以使用Ignore标签,让Room忽略这个构造器
     * Ignore也可用于字段
     * Room不会保存@Ignore注解标记的字段的数据
     */
    @Ignore
    public Student(String name, String age) {
        this.name = name;
        this.age = age;
    }

    //@Embedded 对象嵌套,ForeignTable对象中所有字段 也都会被映射到cache表中,
    //同时也支持ForeignTable 内部还有嵌套对象
    public ForeignTable foreignTable;

	//Realtion注解,关联查询,嵌套对象{entity=对象表user;parentColumn=当前表列名"id",entityColumn=user表列名"id",projection=接收一个数组,包括查询的哪些字段{}}
    @Relation(entity = User.class,parentColumn = "id",entityColumn ="key" ,projection = {"name","age"})
    public User mUSer;
}

 class ForeignTable{
    @PrimaryKey
    @NonNull
    public String foreign_key;

    //@ColumnInfo(name = "_data")
    public byte[] foreign_data;
}

默认情况下,Room将Entity类名作为表名,想单独设置,可通过@Entity注解里的tableName设置。
每个Entity至少有一个字段作为主键,如果想让数据库为字段自动分配ID,可以使用autoGenerate,如果Entity想有符合主键,可以使用@Entity注解里的primaryKeys,设置复合主键。
Room通过@Ignore设置忽略字段,如果Entity继承了父Entity的字段,可以通过@Entity注解里的ignoredColumns属性设置。
Room支持全文搜索,通过使用@Fts3(仅在应用程序具有严格的磁盘空间要求或需要与较旧的SQLite版本兼容时使用)或@Fts4添加到Entity来实现。Room版本须高于2.1.0。
需要注意的是:启用Fts的表必须使用Integer类型的主键,且列名为“rowid”。
如果表支持以多种语言显示内容,可以使用languageId指定用于存储每一行语言信息的列。
如果应用不支持使用全文搜索,可以将数据库的某些列编入索引,加快查询速度,通过@Entity注解添加indices,列出要在索引或符合索引中包含的列名称。
有时候,数据库中的某些字段必须是唯一的,可以通过@Index注解的unique属性设为true,强制实施此唯一属性。如上代码所示可防止nameage同组值的两行。
在 Room 2.1.0 以上版本中,基于 Java 的不可变值类(使用 @AutoValue 进行注释)用作应用数据库中的Entity。此支持在Entity的两个实例被视为相等(如果这两个实例的列包含相同的值)时尤为有用。
将带有@AutoValue 注释的类用作实体时,可以使用 @PrimaryKey@ColumnInfo@Embedded@Relation 为该类的抽象方法添加注释。但是,您必须在每次使用这些注解时添加 @CopyAnnotations 注解,以便 Room 可以正确解释这些方法的自动生成实现。

    @AutoValue
    @Entity
    public abstract class User {
        @CopyAnnotations
        @PrimaryKey
        public abstract long getId();

        public abstract String getFirstName();
        public abstract String getLastName();

        // Room uses this factory method to create User objects.
        public static User create(long id, String firstName, String lastName) {
            return new AutoValue_User(id, firstName, lastName);
        }
    }

创建DAO

最后,我们通过DAO来访问数据。DAO可以是接口,也可以是抽象类,如果是抽象类,则该DAO可以选择有一个以RoomDatabase为唯一参数的构造函数。Room 会在编译时创建每个 DAO 实现。在DAO文件上方添加@DAO注解。

@Dao
public interface CacheDao {
	//插入冲突解决方案,默认ABORT(中止)。REPLACE(替换)。IGNORE(忽略插入数据)。ROLLBACK(回滚)。FAIL(失败)
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    long save(Cache cache);

    /**
     * 注意,冒号后面必须紧跟参数名,中间不能有空格。大于小于号和冒号中间是有空格的。
     * select *from cache where【表中列名】 =:【参数名】------>等于
     * where 【表中列名】 < :【参数名】 小于
     * where 【表中列名】 between :【参数名1】 and :【参数2】------->这个区间
     * where 【表中列名】like :参数名----->模糊查询
     * where 【表中列名】 in (:【参数名集合】)---->查询符合集合内指定字段值的记录
     */

    //如果是一对多,这里可以写List<Cache>
    @Query("select *from cache where `key`=:key")
    Cache getCache(String key);

    //只能传递对象昂,删除时根据Cache中的主键 来比对的
    @Delete
    int delete(Cache cache);

    //只能传递对象昂,删除时根据Cache中的主键 来比对的
    @Update(onConflict = OnConflictStrategy.REPLACE)
    int update(Cache cache);
    
    //运行时候动态配置sql,使用
    //SimpleSQLiteQuery query = new SimpleSQLiteQuery(
   //"SELECT * FROM Song WHERE id = ? LIMIT 1",
   //new Object[]{ songId});
  //Song song = rawDao.getSongViaQuery(query);
    @RawQuery
    Cache getAllCache(SupportSQLiteQuery sqLiteQuery);
}

我们创建好了数据库,定义好了Entity和DAO后,可以操作数据。需要注意,数据操作应在工作线程操作,除非指定在主线程可以查询,否则会发生崩溃。

//在Database中添加获取DAO的抽象实例
 public abstract CacheDao getCache();
//返回 long,这是插入项的新 rowId。
long rowID = StudentDatabase.getInstance().getCache().save(cache);
//返回int,这是删除的行数,更新返回也是int,代表更新的行数
int lines = StudentDatabase.getInstance().getCache().delete(cache);

销毁与重建

如果需要对数据库中的字段类型进行修改,最好的方式就是销毁与重建。
主要包含以下几个步骤:

  1. 创建一张和修改的表同数据结构的临时表。
  2. 将数据从修改的表复制到临时表中。
  3. 删除要修改的表。
  4. 将临时表重命名为修改的表名。
static final Migration MIGRATION_3_4 = new Migration(3, 4) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("CREATE TABLE temp_Student (" +
                    "id INTEGER PRIMARY KEY NOT NULL," +
                    "name TEXT," +
                    "age TEXT)");
            database.execSQL("INSERT INTO temp_Student (id, name, age)" +
                    "SELECT id, name, age FROM Student");
            database.execSQL("DROP TABLE Student");
            database.execSQL("ALTER TABLE temp_Student RENAME TO Student");
        }
    };

预填充数据库

有时候,需要在应用启动的时候就加载一组特定的数据,这就称为预填充数据库。

从应用资源预填充

如需从位于应用assets/目录中的任意位置的预封装数据库文件预填充Room数据库,请先从RoomDatabase.Builder对象调用createFromAsset(),然后再调用build()

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

    private static volatile PreDatabase database;

    private PreDatabase() {

    }

    public static PreDatabase getInstance() {
        if (database == null) {
            synchronized (PreDatabase.class) {
                if (database == null) {
                    Room.databaseBuilder(context, PreDatabase.class, "Sample.db")
                            .createFromAsset("database/myapp.db")
                            .build();
                }
            }
        }
        return database;
    }

}

从文件系统预填充

如果觉得在assets目录下占用应用体积,可以在应用启动时从服务端下载数据库文件到本地,从设备文件系统任意位置(应用的 assets/ 目录除外)的预封装数据库文件预填充Room数据库,请先从 RoomDatabase.Builder 对象调用 createFromFile() ,然后再调用 build()

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

    private static volatile PreDatabase database;

    private PreDatabase() {

    }

    public static PreDatabase getInstance() {
        if (database == null) {
            synchronized (PreDatabase.class) {
                if (database == null) {
                    Room.databaseBuilder(appContext, PreDatabase.class, "Sample.db")
                            .createFromFile(new File("mypath"))
                            .build();

                }
            }
        }
        return database;
    }
}

Room与LiveData和ViewModel的结合

当Room数据库中的数据发生变化时 ,能够通过LiveData组件通知View层,实现数据的自动更新。
首先使用LiveData将返回的数据包装起来。

 @Query("select *from cache")
 LiveData<Cache> getCache();

创建ViewModel,实例化数据库。

public class CacheViewModel extends AndroidViewModel {
   
    private MyDatabase myDatabase;
    private LiveData<Cache> cacheLiveData;

    public CacheViewModel(@NonNull Application application) {
        super(application);
        
        myDatabase=MyDatabase.getInstance(application);
        cacheLiveData=myDatabase.getCacheDao().getCache();
    }
    
    public LiveData<Cache> getCacheLiveData(){
        return cacheLiveData;
    }
}

在Activity中实例化CacheViewModel,监听LiveData的变化。当我们对数据库进行相关操作时,onChanged()会自动调用。

  LiveData<Cache> cacheLiveData = new ViewModelProvider(this).get(CacheViewModel.class).getCacheLiveData();
        cacheLiveData.observe(this, new Observer<Cache>() {
            @Override
            public void onChanged(Cache cache) {
                Log.e("yhj", "onChanged: "+cache.key);           
            }
  });

我之前使用的网络框架是RxJava+Retrofit+SQLite组合使用,学习完Jetpack后,我使用LiveData+Retrofit+Room封装了网络请求缓存框架,将Jetpack组合使用能更好的理解相关组件。
Github地址:https://github.com/hujuny/EasyHttp

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.yanghujun.com/archives/room

Buy me a cup of coffee ☕.