一、Room简介
Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。该库可帮助您在运行应用的设备上创建应用数据的缓存。此缓存充当应用的单一可信来源,使用户能够在应用中查看关键信息的一致副本,无论用户是否具有互联网连接。
二、依赖
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
如果你是使用Java语言开发,同时还要添加下面这项依赖:
annotationProcessor "androidx.room:room-compiler:$room_version"
如果是 使用Kotlin语言开发,那么则是将上面Java环境下的依赖换成下面这项:
kapt "androidx.room:room-compiler:$room_version"
并同时添加下面这个插件:
apply plugin: 'kotlin-kapt'
上面加粗的部分依赖一定要注意,否则恭喜入坑。
三、Room库的三个基础但是很重要的概念:
- Entity
实体类,也就是数据库的一张表结构。使用注解@Entity标记。 - Dao
包含一系列访问数据库的方法。使用注解@Dao标记。 - Database
数据库持有者,是与应用持久化相关数据的底层连接的主要接入点。使用注解@Database标记。
使用**@Database注解需满足三个条件**:
1、定义的类必须是继承于RoomDatabase的抽象类。
2、在注解中需要定义与数据库相关联的实体类列表。
3、包含一个没有参数的抽象方法并且返回一个带有注解的 @Dao。
四、几个注解
Entity:
使用@Entity注解定义的类会被映射为数据库中的一张表。默认表名为实体类的类名,字段名为表名,可以修改。
@Entity(tableName = "JinGuangBuDaiXi")
data class Role(
/**
* roleId字段
* 必须
* 指定,系统会自动生产这个字段的值。请注意上面的<!-----“必须”----->二字
* */
@PrimaryKey(autoGenerate = true) var roleId: Long,
@ColumnInfo(name = "userName") var name: String,
@ColumnInfo(name = "SHIHAO", defaultValue = "jinguangbudaixi") var shihao: String,
@ColumnInfo(name = "kongfu") var wugong: String
) {
// 实际使用中,上面的方法里面,roleId字段必须指定,添加了Ignore的字段也必须指定,但是按理说这两个都不应该必须指定的。
// roleId是id,每次添加的时候自增1,不指定的话,按其默认的规则来。但实际使用的时候,必须指定ID。
// Ignore:既然都说可以忽略了,那么我没有指定你系统忽略就是了,为什么还必须要指定?
}
上面我们看到,又出现了几个新的注解:@Entity、@PrimaryKey、@ColumnInfo和@Ignore,它们的含义如下:
1、@Entity注解中我们传入了一个参数 tableName用来指定表的名称。
2、@PrimaryKey注解用来标注表的主键,并且使用autoGenerate = true 来指定了主键自增长。
3、@ColumnInfo注解用来标注表对应的列的信息比如表名、默认值等等。
4、@Ignore 注解顾名思义就是忽略这个字段,使用了这个注解的字段将不会在数据库中生成对应的列信息。也可以使用@Entity注解中的 ignoredColumns 参数来指定,效果一样。
几种注解的源码如下:
Entity注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Entity {
String tableName() default "";
Index[] indices() default {};
boolean inheritSuperIndices() default false;
String[] primaryKeys() default {};
ForeignKey[] foreignKeys() default {};
String[] ignoredColumns() default {};
}
PrimaryKey注解:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface PrimaryKey {
boolean autoGenerate() default false;
}
ColumnInfo注解:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface ColumnInfo {
String name() default INHERIT_FIELD_NAME;
@SuppressWarnings("unused") @SQLiteTypeAffinity int typeAffinity() default UNDEFINED;
boolean index() default false;
@Collate int collate() default UNSPECIFIED;
String defaultValue() default VALUE_UNSPECIFIED;
String INHERIT_FIELD_NAME = "[field-name]";
int UNDEFINED = 1;
int TEXT = 2;
int INTEGER = 3;
int REAL = 4;
int BLOB = 5;
@IntDef({UNDEFINED, TEXT, INTEGER, REAL, BLOB})
@Retention(RetentionPolicy.CLASS)
@interface SQLiteTypeAffinity {
}
int UNSPECIFIED = 1;
int BINARY = 2;
int NOCASE = 3;
int RTRIM = 4;
@RequiresApi(21)
int LOCALIZED = 5;
@RequiresApi(21)
int UNICODE = 6;
@IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
@Retention(RetentionPolicy.CLASS)
@interface Collate {
}
Ignore注解:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface Ignore {
}
Dao
Dao类是一个接口interface,其中定义了一系列的操作数据库的方法。通常我们操作数据库无非就是增、删、改、查。Room也为我们的提供了相关的注解,有@Insert、@Delete、@Update 和 @Query。
@query:查询注解,参数是String类型,我们直接写SQL语句进行执行,而且编译的时候可以进行语法检查。
@Query("select * from role where userId = :id")
fun getRoleById(id: Long): Role
SQL语句引用传递的参数直接使用 :符号进行引用。
上面Query后面的sql语句中,user是指我们所建的实体类;userId则是我们创建实体类时指定的那个Long型数据的Id。我们还可以这么改:
@Query("select * from Role where name = :mName")
fun getRoleById(mName: String): Role
@Insert:如果我们需要向表中插入一条数据,我们直接定义一个方法并用 @Insert注解标注就可以:
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addRole (mRole: Role)
我们看到直接中有个参数onConflict,表示当插入的数据已经存在时候的处理逻辑,有三种操作逻辑:REPLACE、ABORT和IGNORE。如果不指定则默认为ABORT终止插入数据。这里我们将其指定为REPLACE替换原有数据。
@Delete:如果需要删除表的数据则使用 @Delete注解:
@Delete
fun deleteRoleByRole(mRole: Role): 使用主键来查找要删除的实体。
@Update: 如果需要修改某一条数据则使用 @Update注解,和@Delete一样也是根据主键来查找要删除的实体。
@Update
fun updateRoleByRole (mRole: Role)
上面说的 @Query 查询接受的参数是一个字符串,所以像删除或者更新我们也可以使用 @Query 注解来使用SQL语句来直接执行。比如根据userid来查询某个用户或者根据userid更新某个用户的姓名:
@Query("delete from role where userId = :id ")
fun deleteRoleById(id:Long)
@Query("update role set userName = :updateName where userID = :id")
fun update(id: Long, updateName: String)
最终Dao的结构如下:
@Dao
interface RoleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUser(mRole:Role)
//Insert 可以单独插入,也可以批量插入
@Insert
fun insertUser(mRole:Role)
@Insert
fun insertRoles(mRole:List<Role>)
//Query的值即使要执行的sql语句
@Query("SELECT * FROM JinGuangBuDaiXi")
fun getAll():List<Role>
@Query("SELECT * FROM JinGuangBuDaiXi WHERE userName in (:names)")
fun getRolesByMames(names:List<String>):List<Role>
//注意点:1、在Room使用中,模糊匹配的格式为" like '%' || :userName || '%' ",即在要匹配的参数值前后要加上 “||”,并且“%”要区分出来
// 2、下面查询用到的变量的名字要跟实体类(比如本demo中的Role.kt)中的变量要保持一致,否则查询失败
// 3、WHERE后面的变量(比如本例中的“userName”)要用实体类(比如本demo中的Role.kt)中注解后面的“name”字段的值,否则查询失败。
@Query("SELECT * FROM JinGuangBuDaiXi WHERE userName like '%'|| :name ||'%' LIMIT 1")
fun getRoleInfoByName(name:String):Role
@Query("SELECT * FROM JinGuangBuDaiXi WHERE kongfu like '%'|| :wugong ||'%' LIMIT 1")
fun getRoleInfoBykONGfU(wugong:String):Role
@Query("SELECT * FROM JinGuangBuDaiXi WHERE SHIHAO like '%'|| :shihao ||'%' LIMIT 1")
fun getRoleInfoByShiHao(shihao:String):Role
@Update
fun updateRoleByRole(user: Role)
@Delete
fun deleteRoleByRole(mRole: Role)
}
Database
首先定义一个抽象类继承RoomDatabase类,并添加注解 @Database 来标识:
@Database(version = 1, entities = [Role::class])
abstract class RoleDatabase : RoomDatabase() {
abstract fun roleDao():RoleDao
companion object {
private var instance: RoleDatabase? = null
fun getInstance(context: Context): RoleDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context, RoleDatabase::class.java, "Role.db" ).allowMainThreadQueries().build()//"Role.db" :数据库名称
}
return instance as RoleDatabase
}
}
}
使用entities来映射相关的实体类,version来指明当前数据库的版本号。这里使用了单例模式来返回Database,以防止新建过多的实例造成内存的浪费。
Room.databaseBuilder(context,klass,name).build()来创建Database,其中第一个参数为上下文,第二个参数为当前Database的class字节码文件,第三个参数为数据库名称。
默认情况下Database是不可以在主线程中进行调用的。因为大部分情况,操作数据库都还算是比较耗时的动作。如果需要在主线程调用则使用allowMainThreadQueries进行说明。
具体使用:
几个关键的步骤都完成后,接下来我们自然是开始进入使用阶段:
class RoomSqlActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_room_sql)
var roleDao = RoleDatabase.getInstance(this).roleDao()
var name = "任缥缈"
var shihao = "风满楼,卷黄沙,舞剑春秋,名震天下。雨飘渺,倦红尘,还君明珠,秋水浮萍。"
var wugong = "飘渺剑法:虚无飘渺,无迹可寻,精妙绝伦,变幻莫测。无形无相"
btnAdd.setOnClickListener {
var role01 = Role(1L, name, shihao, wugong)
roleDao.addUser(role01)
name = "俏如来"
shihao = "红尘轮回众生顾,因果循环有定数,放下屠刀虽成佛,愿坠三途灭千魔。"
wugong =
"圣印六式;诛魔之利:止戈流·「鬼破、星流、星陨、星痕、月痕、日殒、破日、天誓」;止戈流·真阵·「初式·十剑山河荡狼烟、继式·十面江海靖尘嚣、终式·十万沙劫漫云天」"
var role02 = Role(2L, name, shihao, wugong)
roleDao.addUser(role02)
name = "藏镜人"
shihao = "踏烽火,折兵锋,正邪无用;斩敌颅,杀魍魉,天地不容"
wugong = "以纯阳金刚体强行逆练纯阴功法:飞瀑掌;纯阳掌;纯阳行左·飞瀑走右·阴行阳招·阳行阴招·阴阳融合·袭地贯天;啸问岁月·天狼影"
var role03 = Role(3L, name, shihao, wugong)
roleDao.addUser(role03)
name = "遥星公子别小楼"
shihao = "沉刀埋霜小楼庭,回首江湖风云轻。君有才能纵捭阖,清溪仰望有遥星"
wugong =
"纵横诀·『吴钩霜月明、飒沓如流星、千里不留行、十步杀一人、赵客缦胡缨、银鞍照白马;星月合招:十方萧索无涯·千古夕阳有主·诗仙纵横·刀剑茫茫去不还』、碧海定涛掌"
var role04 = Role(4L, name, shihao, wugong)
roleDao.addUser(role04)
println("TAGTAG:执行了添加操作")
}
btnQuery.setOnClickListener {
roleDao.getAll().forEach() {
println("TAGTAG:执行了查询操作:${it.toString()}")
}
}
btnInsert.setOnClickListener {
name = "宫本总司"
shihao = "萧无名、曲无名、声幽幽、声悲鸣,心何闷?情何困?眉深锁、孤独行。"
wugong = "无常无定。神魔一念:以魔入心,以神出招,心法极意,不在魔心控杀,而在神意止杀,忘情忘仇,亦神亦魔,神魔非我。一剑无极、一剑无尽、一剑无声、一剑无悔"
var roleInsert = Role(5L, name, shihao, wugong)
roleDao.insertUser(roleInsert)
println("TAGTAG:执行了 插入 操作")
}
btnQuery.setOnClickListener {
name = "任缥缈"
var role = roleDao.getRoleInfoByName(name);
println("TAGTAG:通过${name}查询到的信息:${role?.toString()}")
wugong = "飘渺剑法:虚无飘渺,无迹可寻,精妙绝伦,变幻莫测。无形无相"
role = roleDao.getRoleInfoBykONGfU(wugong)
println("TAGTAG:通过${wugong}查询到的信息:${role?.toString()}")
shihao = "萧无名、曲无名、声幽幽、声悲鸣,心何闷?情何困?眉深锁、孤独行。"
role = roleDao.getRoleInfoByShiHao(shihao)
println("TAGTAG:通过${shihao}查询到的信息:${role?.toString()}")
var nameList = listOf<String>("任缥缈", "俏如来")
var nameQueryList = roleDao.getRolesByMames(nameList)
for (item in nameQueryList){
println("通过namelist查询到的结果:${item.toString()}")
}
}
btnUpdate.setOnClickListener {
name = "俏如来"
shihao = "天堂地狱一道门,道门无扉三朵云。云中难觅五形气,气化心逢七彩君。"
wugong = "圣印六式;诛魔之利:止戈流·「鬼破、星流、星陨、星痕、月痕、日殒、破日、天誓」;止戈流·真阵·「初式·十剑山河荡狼烟、继式·十面江海靖尘嚣、终式·十万沙劫漫云天」"
var roleUpdate = Role(2L, name, shihao, wugong)
roleDao.updateRoleByRole(roleUpdate)
roleUpdate = roleDao.getRoleInfoByName(name)
println("TAGTAG:更新后的结果:${roleUpdate.toString()}")
}
btnDelete.setOnClickListener {
name = "俏如来"
shihao = "天堂地狱一道门,道门无扉三朵云。云中难觅五形气,气化心逢七彩君。"
wugong = "圣印六式;诛魔之利:止戈流·「鬼破、星流、星陨、星痕、月痕、日殒、破日、天誓」;止戈流·真阵·「初式·十剑山河荡狼烟、继式·十面江海靖尘嚣、终式·十万沙劫漫云天」"
var roleUpdate = Role(2L, name, shihao, wugong)
roleDao.deleteRoleByRole(roleUpdate)
var list = roleDao.getAll()
for (item in list){
println("TAGTAG:删除指定数据:俏如来后查询到的结果:${item.toString()}")
}
}
}
}
运行结果:
数据库的升级和降级:
在使用数据库的时候避免不了的就是数据库的更新。数据库的升级或者降级使用addMigrations方法进行操作, 然后完整的Database应该是这样的:
@Database(version = 1, entities = [Role::class])
abstract class RoleDatabase : RoomDatabase() {
abstract fun roleDao():RoleDao
companion object {
private var instance: RoleDatabase? = null
/**
*startVersion:表示的是升级开始的版本
* endVersion:表示要升级到的目标版本,endVersion>startVersion
*/
fun getInstance(context: Context, isUpdateVersion:Boolean, startVersion:Int, endVersion:Int): RoleDatabase {
if (instance == null) {
if (isUpdateVersion){ //"Role.db" :数据库名称
instance = Room.databaseBuilder(context,RoleDatabase::class.java,"Role.db" ).allowMainThreadQueries().build()
}else{
/**
* startVersion表示的是升级开始的版本,
* endVersion表示要升级到的目标版本,endVersion>startVersion。
* 同时需要将@Database注解中的version的值修改为和endVersion相同。
* 数据库的升级或者降级使用addMigrations方法进行操作
*/
instance = Room.databaseBuilder(context,RoleDatabase::class.java,"Role.db"
).addMigrations(object : Migration(startVersion,endVersion){
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user ADD age INTEGER Default 0 not null ")
}
}).allowMainThreadQueries().build()
}
}
return instance as RoleDatabase
}
}
}
Migration方法有两个参数:
startVersion:表示的是升级开始的版本。
endVersion:表示要升级到的目标版本,endVersion>startVersion。同时需要将@Database注解中的version的值修改为和endVersion相同。
当在升级或者降级的过程中出现版本未匹配到的情况的时候,默认情况下会直接抛异常出来。当然我们也可以处理异常。
升级的时候可以添加fallbackToDestructiveMigration方法,当未匹配到版本的时候就会直接删除表然后重新创建。
降级的时候添加fallbackToDestructiveMigrationOnDowngrade方法,当未匹配到版本的时候就会直接删除表然后重新创建。