首语
Android使用SQLite作为数据库存储数据,但是SQLite使用繁琐且容易出错,有许多开源的数据如GreenDAO、ORMLite等,这些都是为了方便SQLite的使用而出现的,Google也意识到了这个问题,在Jetpack组件中推出了Room,Room在SQLite上提供了一层封装,可以流畅的访问数据库。
优势
- 拥有SQLite的所有操作功能。
- 使用简单,通过注解的方式实现相关功能,编译时自动生成实现类impl。
- 与LiveData、LifeCycle及Paging天然支持。
依赖
代码语言:javascript复制 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架构图如图所示。
使用
创建数据库。
代码语言:javascript复制//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
代码语言:javascript复制@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
,强制实施此唯一属性。如上代码所示可防止name
和age
同组值的两行。
在 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后,可以操作数据。需要注意,数据操作应在工作线程操作,除非指定在主线程可以查询,否则会发生崩溃。
代码语言:javascript复制//在Database中添加获取DAO的抽象实例
public abstract CacheDao getCache();
代码语言:javascript复制//返回 long,这是插入项的新 rowId。
long rowID = StudentDatabase.getInstance().getCache().save(cache);
//返回int,这是删除的行数,更新返回也是int,代表更新的行数
int lines = StudentDatabase.getInstance().getCache().delete(cache);
销毁与重建
如果需要对数据库中的字段类型进行修改,最好的方式就是销毁与重建。 主要包含以下几个步骤:
- 创建一张和修改的表同数据结构的临时表。
- 将数据从修改的表复制到临时表中。
- 删除要修改的表。
- 将临时表重命名为修改的表名。
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将返回的数据包装起来。
代码语言:javascript复制 @Query("select *from cache")
LiveData<Cache> getCache();
创建ViewModel,实例化数据库。
代码语言:javascript复制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