Android 最实用的Room入门

一. 简介与导入

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(tableName = "student", primaryKeys = ["name"])

也可以指定联合主键:

@Entity(tableName = "student", primaryKeys = ["name", "id"])

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,进行异步操作,需要引入依赖

implementation "androidx.room:room-rxjava2:2.3.0"

比如:

@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:

 AppDatabase.getInstance(context!!.applicationContext).openHelper.writableDatabase.update("student", SQLiteDatabase.CONFLICT_ROLLBACK,values, selection, selectionArgs )
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟红尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值