文章目录
-
- 一. 简介与导入
- 二 . 应用
-
-
- 1. 利用注解entity定义实体类
- 2. 定义Dao,用于操作数据,进行增删改查
- 3. 定义database
- 4. 数据库的升级与降级
- 5. 表关联
-
- 三. 其他可能会用的一点技巧
-
-
- TypeConverter
- Embedded
- rxjava2
- 补充
-
一. 简介与导入
Andorid官方中推荐Room代替SQlite,所以新的项目中直接舍弃了以前用的第三那方框架greenDao
Room由三部分组成,并且用三个注解标注:
Entity: 这个注解表示的是实体类,代表的是数据库中的表,每一个实体类都是一张表
Dao:改注解标注的是一个接口,接口中封装的是操作数据库的方法,比如增删改查
database:这个注解标注的是一个数据的持有者,他是一个抽象类,并且持有一个接口dao的抽象方法,还需在这个抽象类中添加上所有实体的标识,另外他需要继承RoomDatabase。其实原理是在编译阶段,根据注解,就会编译出具体的实现类,所以对于我们来说这就简单的很多,因为所有的代码都是编译器根据规则生成的。
所以我们需要再gradle中加入:
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
需要依赖:
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-ktx:2.3.0"
加入kapt的目的就是编译注解。kapt是一个注解处理插件
二 . 应用
1. 利用注解entity定义实体类
比如:
@Entity(tableName = "student")
data class Student(
val name: String,
val age:String,
) {
@PrimaryKey(autoGenerate = true)
var id:Long = 0L
}
Entity注解标记这个类是一个表,可以自定义表明tableName,如果不指定的话默认是类的名字,一个表中必须指定一个主键,如果不指定主键编译会报错,可以使用注解@PrimaryKey 指定某一个属性为主键,其中autoGenerate表示的是自增,如果不在属性上标记主键的话,可以再@Entity上面指定,比如指定姓名为主键:
也可以指定联合主键:
Entity这个注解还有其他的几个值,但是不怎么常用
Index[] indices() default {}; 索引,可以为表添加索引
boolean inheritSuperIndices() default false; 表示的是父类的索引是否可以被当前的类继承
ForeignKey[] foreignKeys() default {}; 需要依赖的外键,现在基本上没人会用外键
,都是根据逻辑关系控制具体的数据
String[] ignoredColumns() default {}; 可以忽略的字段
当数据量很大的时候,可以为了快速查找,可以为某一列或者多列添加索引,不过一般应该用不到,主要是Android端一般也不会存储特别大的数据量
@Entity(tableName = "student",
indices = [Index("name"), Index(value = ["name", "age"])])
还有一个就是外键,用处不大,一般没人用,了解下就好:
@Entity(tableName = "student",
foreignKeys = [ForeignKey(entity = ClassRoom::class,parentColumns = ["id"],
childColumns = ["classRoomId"])])
parentColumns 指的是所依赖的表的主键,childColumns 当前表中的所依赖的表的id
默认情况下,Entity标注的类中的属性值都是表中的一列,如果某一列不需要存储在表中,可以用ignoredColumns 进行忽略,不过一般都是用单独的注解进行忽略,比如:
@Ignore
val classRoomId:String
如果列不想用默认的属性名字的话可以用@ColumnInfo进行指定:
@ColumnInfo(name = "age")
val age:String,
ColumnInfo这个注解下面有几个属性可以使用:
String defaultValue() default VALUE_UNSPECIFIED; 设置默认值
boolean index() default false; 是不是索引列
@Collate int collate() default UNSPECIFIED; 列的排列顺序
设置默认值可能会用,其他的两个基本上也不会用
2. 定义Dao,用于操作数据,进行增删改查
定义的dao是一个接口,用Dao进行标注,例如查找所有的学生:
@Dao
interface StudentDao {
@Query("select * from student")
fun findAll():List<Student>
}
如果根据条件查询的话,那可可以进行将值传过来,写where语句例如:
@Query("select * from student where id = :id ")
fun findById(id:Long): Student
:后面的参数即为方法中的参数,需要多少传多少即可
其实这样一个方法的上面只要写sql就好了,还是很简单的,在比如,相求 总的数据条数:
@Query("select count(*) from student")
fun findCount():Int
是不是很简单,当然如果你想联合查询某几个表,只是保留某几个字段的话,那就需要指定列名,并且和你返回的实体类的属性一一对应即可,比如:
@Query("select s.name as studentName, r.class_name as roomName from student as s left join class_room as r on r.class_id = s.roomId")
fun getAllName():List<StudentRoom>
data class StudentRoom(val studentName:String,val roomName:String) {}
insert也很简单,直接用@Insert注解即可
@Insert
fun insertStudent(student: Student)
如果想确定是不是插入成功的的话,可以加一个返回参数的,返回的是你插入的id吧
当然可以批量插入,用可变参数或者list都可以,返回的结果也是一个list,表示每一个插入成功的id
其实编译之后,可以点进去看看源码的:
@Override
public long insertStudent(final Student student) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
long _result = __insertionAdapterOfStudent.insertAndReturnId(student);
__db.setTransactionSuccessful();
return _result;
} finally {
__db.endTransaction();
}
}
明显返回的是ID
也可以用可变参数,kotlin是vararg表示可变参数
更新和删除的话,可以这么干:
@Update
fun updateStudent(student: Student)
@Delete
fun deleteStudent(student: Student)
都是根据主键进行更新和删除的,但是这里有一个问题是,更新的话就对于这个主键的一行数据全部更新了,所以有时候只需要更新某一个字段的话,就需要用另外一种方法了,使用sql语句
比如 我只想更新学生的名字:
@Query("update student set name = :name where id = :id")
fun updateNameForStudent(name:String,id:Long)
同理,delete其实也可以写sql语句
3. 定义database
有了表和操作方法,没有数据库,所以我们需要定义一个数据库,数据库中指定该数据库中有哪些表和操作方法,需要一个抽象方法,并继承RoomDatabase,如下:
@Database(entities = [Student::class, ClassRoom::class], version = 1)
abstract class AppDatabase : RoomDatabase(){
abstract fun studentDao(): StudentDao
companion object {
@Volatile
private var INSTANCE: AppDatabase?= null
fun getInstance(context: Context): AppDatabase =
INSTANCE?: synchronized(this){
INSTANCE?:buildDatabase(context).also {
INSTANCE = it
}
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
.allowMainThreadQueries()
.build()
}
}
entities 指定所有的实体类,version指定当前数据库的版本,并且里面有一个抽象方法,返回值就是你的dao,这就可以了,剩下的编译插件会自动给你实现。接下来就是创建数据库了,创建书数据库的方法是这个:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
.allowMainThreadQueries()
.build()
指定你的数据库抽象类和数据库的名称,并创建,这个地方法你可以再任何用到数据库的地方执行,但是通常我们会写再database中,并改造成单例模式,这是避免有多个数据的引用,避免资源的浪费,到这儿基本就结束了,剩下的就是调用了, .allowMainThreadQueries() 这个方法事运行在主线程中调用,如果不加的话,在主线程调用会报错的,比如插入一个学生:
AppDatabase.getInstance(this).studentDao()
.insertStudent(Student("Marry","24",1))
4. 数据库的升级与降级
表的增加,表的修改,是避免不了的,所以这个时候需要表的升级
比如我增加了一个地址表的话
就应该,加上address这个实体类,并将版本号升级
@Database(entities = [Student::class, ClassRoom::class, Address::class],
version = 2)
如果只是这样,那app就挂了,还需要加上一些其他的处理,有时候升级的时候,可能需要删除某些数据,并将数据迁移等等,还需要一个方法:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
.addMigrations(MIGRATION_1_2)
.allowMainThreadQueries()
.build()
private val MIGRATION_1_2 = object : Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {
}
}
每升级一次,都需要加一个这个方法,比如2到3
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.allowMainThreadQueries()
.build()
private val MIGRATION_1_2 = object : Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {
}
}
private val MIGRATION_2_3 = object : Migration(2,3){
override fun migrate(database: SupportSQLiteDatabase) {
}
}
升级的规则是每升一级都要有一个相应的方法,如果直接第一个版本升级到第三个版本的话,默认会先执行升级到2,然后升级到3
另外还有一个特别需要注意的问题是,如果添加了新表的话,不只需要在database这个类中entities 指定,还需要再升级的时候去写sql创建表,才行,否则会报错。
比如我想加一个address的表,
@Database(entities = [Student::class,
ClassRoom::class, Address::class], version = 2)
private val MIGRATION_1_2 = object : Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `address` (`addressId` INTEGER NOT NULL, `addressName` TEXT NOT NULL, PRIMARY KEY(`addressId`))")
}
}
这样才行,另外不要自己写这个sql语句,去你自动生成的创建语句copy,否则,自己写如果和自动生成的create语句不一致的话,也会报错。
默认生成的create的语句在你的database_impl这个类中
public void createAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("CREATE TABLE IF NOT EXISTS `student` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `age` TEXT NOT NULL, `roomId` INTEGER NOT NULL)");
_db.execSQL("CREATE TABLE IF NOT EXISTS `class_room` (`class_id` INTEGER NOT NULL, `class_name` TEXT NOT NULL, PRIMARY KEY(`class_id`))");
_db.execSQL("CREATE TABLE IF NOT EXISTS `address` (`addressId` INTEGER NOT NULL, `addressName` TEXT NOT NULL, PRIMARY KEY(`addressId`))");
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
_db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7cbdd6263025181ec070edd36e1118eb')");
另外升级添加字段的时候,一定不要忘了 添加默认值,否则也会有问题的
如果数据库要降级的话需要添加这.fallbackToDestructiveMigration(),默认删除所有的表,重新创建,当然所有的数据也就没了
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
5. 表关联
Room也是关系型数据库,所以表和表之间可以有一对一的关系,一对多的关系,多对多的关系
一对一的关系
比如一个学生对应一个教室:根据这个学生查出所对应的教室:
学生表:
@Entity(tableName = "student")
data class Student(
val name: String,
val age:String,
val roomId:Long
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id:Long = 0L
}
教室表:
@Entity(tableName = "class_room")
data class ClassRoom(@PrimaryKey val class_id:Long, var class_name:String) {}
查询所对应的关系表:
data class StudentRoom(
@Embedded val student: Student,
@Relation(
parentColumn = "roomId",
entityColumn = "class_id"
)
val classRoom: ClassRoom
) {}
对应的查询语句:
@Query("select * from student")
fun getAllStudent():List<StudentRoom>
多对一的的话,那就是一个教室对应多个学生,相对来说也很简单:
data class StudentRoom(
@Embedded val student: Student,
@Relation(
parentColumn = "roomId",
entityColumn = "class_id"
)
val classRoom: ClassRoom
) {}
查询语句:
@Query("select * from class_room")
fun getAllRoom():List<RoomStudent>
还有一种多对多的关系,暂时先不看了,也用不到
三. 其他可能会用的一点技巧
TypeConverter
有时候数据库中的一些数据是无法存储的,比我Student,每一个学生都有些朋友,朋友很多,我只想存储下他的名字,那就是一个list:
data class Student(
val name: String,
val age:String,
val roomId:Long,
val friend:List<String>
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id:Long = 0L
}
这个时候肯定会报错,因为数据库不明白你这个list是什么东西,所以我们可以对他进行转换,比如说转成string类型,所以就有了一个注解TypeConverter:
class MyConverters {
@TypeConverter
fun listToString(value:List<String>):String{
val sb = StringBuilder()
value.forEach {
sb.append(",").append(it)
}
return sb.toString().substring(1)
}
@TypeConverter
fun stringToList(value:String):List<String>{
return value.split(",")
}
}
定义一个类,类中有两个方法,一个是转string,一个是转list,这两个一定要成对出现,用于实现自动转换,接着在实体类上标注下即可:
@Entity(tableName = "student")
@TypeConverters(MyConverters::class)
data class Student(
val name: String,
val age:String,
val roomId:Long,
val friend:List<String>
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id:Long = 0L
}
系统会自动转list和string,比如insert语句中,生成的代码:
_tmp = __myConverters.listToString(value.getFriend());
public void bind(SupportSQLiteStatement stmt, Student value) {
stmt.bindLong(1, value.getId());
if (value.getName() == null) {
stmt.bindNull(2);
} else {
stmt.bindString(2, value.getName());
}
if (value.getAge() == null) {
stmt.bindNull(3);
} else {
stmt.bindString(3, value.getAge());
}
stmt.bindLong(4, value.getRoomId());
final String _tmp;
_tmp = __myConverters.listToString(value.getFriend());
if (_tmp == null) {
stmt.bindNull(5);
} else {
stmt.bindString(5, _tmp);
}
}
};
Embedded
还有一个注解 可能也会用到 @Embedded,这个表示嵌套对象
比如Student中我使用@Embedded 嵌套一个实体类,这个实体类就是一个普通的类
@Entity(tableName = "student")
data class Student(
val name: String,
val age:String,
val roomId:Long,
@Embedded val test: Test
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id:Long = 0L
}
data class Test(val testName:String) {
}
test中表的字段会在创建student表的时候创建出来,所以student会多出testName这一列。
rxjava2
还可以使用rxjava,进行异步操作,需要引入依赖
比如:
@Query("select * from student")
fun getAllStudent():Observable<List<StudentRoom>>
补充
Andorid中contentProvider是非常常用的四大主件之一,用于提供数据,当我们使用了room之后,怎么对应contentProvider中的查询,删除,修改呢?
AppDatabase.getInstance(context!!.applicationContext).openHelper
.writableDatabase.query(SupportSQLiteQueryBuilder
.builder("student")
.selection(selection, selectionArgs)
.columns(projection).orderBy(sortOrder).create())
用到的是SupportSQLiteQueryBuilder,进行怎删改语句的创建,用来接收传过来的参数,是不是很简单?
再比如update: