废话也不多说了,Room数据库就是对SQLite数据库的封装,使之用起来更方便。Google也说了强烈推荐使用Room来替代SQLite。
dependencies {
...
def room_version = "2.1.0-alpha02"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
由于使用了Kotlin注解,请确保Gradle里配置了kotlin-kapt插件
apply plugin: 'kotlin-kapt'
Room数据库主要由三个部分组成:
Entity
: 数据实体,一个Entity代表一张数据表DAO
: 在这里定义数据表的操作方法Database
: 数据库下面详细来讲如何实现这三个部分。
@Entity(tableName = "user")
data class UserEntity(@PrimaryKey @ColumnInfo(name = "uid") var uid: Int,
@ColumnInfo(name = "first_name") var firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?)
要点1:在类上方添加@Entitiy
注解,并定义数据表名称
要点2:通过@PrimaryKey
来定义主键,@ColumnInfo
来定义列名称,也可以不添加注解@ColumnInfo
,那么列名称就默认是属性的名称。
这样就相当于定义了一张表user
,user表包含uid
,first_name
, last_name
三个字段。其中uid
为主键,数据类型为Int
, first_name
和last_name
数据类型为String
,且可以为空。
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<UserEntity>
@Query("SELECT * FROM user WHERE uid = (:userId)")
fun findById(userId: Int): UserEntity
@Insert
fun insertAll(vararg userEntities: UserEntity)
@Delete
fun delete(vararg user: UserEntity)
@Update
fun update(vararg user: UserEntity)
}
可以看到,在这里通过各种注解实现了数据表的各种增删改查方法。其中delete,并不是说要传入一个完全一样的实体,只要PrimaryKey
匹配就行。具体就不多讲了,一看就懂。
最重要的是要在接口前面添加注解@Dao
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase(){
//获取数据表操作实例
abstract fun userDao(): UserDao
//单例模式
companion object {
private const val DB_NAME = "app_database"
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase{
val tempInstance = INSTANCE
if(tempInstance != null) { return tempInstance }
synchronized(this){
val instance = Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, DB_NAME).build()
INSTANCE = instance
return instance
}
}
}
}
直接甩代码了,最重要的当然是要在类前面添加注解@Database
,在entities
中添加所有数据表的类名,version
表示数据库版本,exportSchema
表示是否生成数据库结构文件,具体可以参考: 处理Schema
数据库初始化方法如下:
val db = Room.databaseBuilder(applicationContext,
AppDatabase::class.java, "database-name").build()
要点1. 自定义的Database类必须继承RoomDatabase。
要点2. 在Database中定义获取数据表操作实例的方法。
要点3. 谷歌建议把Database实例定义成单例模式,因为数据库的初始化消耗量相当大,而且也没必要在一个进程中创建多个数据库实例。
然后就可以操作数据库了,比如向数据表user插入一行数据,如下:
val user = UserEntity(1, "rx", "chen")
AppDatabase.getDatabase(this).userDao().insertAll(user)
插入之后查看user数据表:
重要提示:Room数据库的操作不能在UI线程执行,请勿在主线程直接调用上述代码
除了user
表,我还有一张login
表,记录用户的登录次数和最后登录时间,如下:
@Entity(tableName = "login")
data class LoginEntity(@PrimaryKey @ColumnInfo(name = "id") var uid: Int,
@ColumnInfo(name = "login_count") var loginCount: Int,
@ColumnInfo(name = "latest_time") var latestTime: String?)
现在我想知道每个用户的登录次数,得到每个用户的名字和登录次数。
首先,对应我们需要数据,建立一个相应的数据类,以此来接受获取的数据
class UserLoginCount(var last: String, var first: String, var loginCount: Int) {
val userName: String
get() = "$first $last"
override fun toString(): String {
return "name: $userName, loginCount: $loginCount"
}
}
然后创建一个联表查询的DAO
,在其中定义好查询方法:
@Dao
interface JoinDao {
@Query("SELECT user.first_name as first, " +
"user.last_name as last, " +
"login.login_count as loginCount " +
"FROM login, user WHERE user.uid = login.id ")
fun getUserLoginCount(): List<UserLoginCount>
}
在Database类中定义获取JoinDao的方法:
abstract fun joinDao(): JoinDao
最后就可以直接查询了:
val userLoginCounts = AppDatabase.getDatabase(this).joinDao().getUserLoginCount()
打印出userLoginCounts,结果如下:
name: rx chen, loginCount: 12
name: julia jx, loginCount: 3
现在我们给user
表添加一个字段age
,数据库版本从原来的1
升到2
,Database类修改如下:
@Database(entities = [UserEntity::class, LoginEntity::class], version = 2, exportSchema = false)
abstract class AppDatabase : RoomDatabase(){
//获取数据表操作实例
abstract fun userDao(): UserDao
abstract fun loginDao(): LoginDao
abstract fun joinDao(): JoinDao
//单例数据库
companion object {
private const val DB_NAME = "app_database"
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase{
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room
.databaseBuilder(context.applicationContext,
AppDatabase::class.java, DB_NAME)
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_1_2)
.build()
INSTANCE = instance
return instance
}
}
//升级语句
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
}
}
同时,别忘了给UserEntity添加age属性
@Entity(tableName = "user")
data class UserEntity(@PrimaryKey @ColumnInfo(name = "uid") var uid: Int,
@ColumnInfo(name = "first_name") var firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?,
@ColumnInfo(name = "age") var age: Int = 0)
这里最重要的是数据库初始化的时候添加了两个方法:
一个是 fallbackToDestructiveMigration()
,这个方法的作用是,当数据库升级无法匹配时,比如执行的升级是从版本2升到版本3,但是却找不到版本2(有可能这时仍然是版本1),此时就会按照最新的版本3重建数据库,那么之前的数据库就会被删除,数据也会被清掉。如果不添加这个方法,遇到这种情况会导致app崩溃。
第二个方法当然是添加升级的方法addMigrations()
,这个方法可以接受多个升级语句,比如当前要升级到版本3,但考虑到一部分用户已经是版本2,一部分用户仍然在用版本1,那么此时应该提供两种升级方式:从版本1升级到版本3 和 从版本2升级到版本3。写好两种升级语句,一起传给addMigrations()
方法即可。
执行数据库初始化方法后,数据表user
已经添加了age
字段,如下:
PS:数据库升级时写的修改数据表字段或者添加数据表的SQL语句,一定要和Entity定义的内容严格对应,比如我在
UserEntity
中定义的age
字段是非空的,那么SQL语句中就一定要写上age INTEGER NOT NULL
,不然就会升级失败甚至导致app崩溃。