概述
room是Google官方推荐的一个数据库Sqlite框架。Room在SQLite上提供了一个抽象层,以便在充分利用Sqlite的功能的同时也能够访问数据库。
依赖引入
首先需要在gradle中引入room依赖包,示例如下:
dependencies {
//版本
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
//optional - rxjava for room
implementation "androidx.room:room-rxjava2:$room_version"
}
Room结构
Room主要包含有三个组件:DataBase(数据库),Entity(数据表结构),DAO(数据表操作)。
常规的操作流程如下:
- APP使用DataBase来获取与该数据关联的数据表操作对象:DAO。
- 应用使用每个DAO从数据库中获取实体:Entity。
- 应用对Entity进行更改或获取数据。
- 应用将对Entity的更改通过DAO保存到数据库中。
具体的结构如下图所示:
DataBase
Database主要是包含了DAO并且提供创建和链接数据库的方法。
1. 创建数据库
创建DataBase主要包括如下几个步骤:
- 创建继承
RoomDatabase
的抽象类 - 在继承类前使用注解
@Database
- 声明数据库结构的Entity并设置数据库版本号。
- 数据库实例最好能够定义为单例。
示例代码如下:
@Database(entities = [TestUserEntity::class], version = 2, exportSchema = false)
abstract class TestDataBase : RoomDatabase() {
//获取DAO数据库操作
abstract fun getDao(): ITestUserDao
//单例模式
companion object {e
private const val DB_NAME = "test_user_database"
@Volatile
private var INSTANCE: TestDataBase? = null
fun getDatabase(context: Context): TestDataBase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TestDataBase::class.java, DB_NAME
).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")
}
}
}
}
数据库迁移
当升级Android应用时,有时需要更改数据库中的数据结构,要用户升级应用的时候保持原有的数据不变。此时就需要用到数据迁移Migration。
Room中通过支持Migration类
进行增量迁移以满足此需求。通过设置不同的Migration对象完成版本升级中的变动。当应用更新需要升级数据库版本时,Room会从一个或者多个Migration对象中运行migrate()
方法,实现数据库版本升级。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
"PRIMARY KEY(`id`))")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
}
}
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
注意事项
数据迁移必须完整的定义所有版本的迁移,否则如果没有对应版本的Migration时,Room数据库会清楚原来的数据重写创建。
DAO
DAO全称:data access object,可以通过SQL语句进行对数据库的操作并且将这些语句和JAVA中的方法关联调用,编译器会检查SQL语句并且通过注解生成对应的SQL语句及数据库操作。
在Room中,需要被设置为DAO需要注意如下几点:
- DAO必须是抽象类或者接口。
- DAO必须要使用
@Dao
注解进行标识。
DAO示例代码如下:
@Dao
interface ITestUserDao {
//采用suspend关键字使用kotlin协程使得数据库的操作方法不在主线程进行
@Query("select * from test_user")
fun getAll(): List<TestUserEntity>
@Query("select * from test_user where id = (:userId)")
fun getById(userId: Long): Flowable<TestUserEntity>
@Update
suspend fun updateUserInfo(vararg userEntity: TestUserEntity)
@Insert
fun insertUser(vararg userEntity: TestUserEntity)
@Delete
fun deleteUser(vararg userEntity: TestUserEntity)
@Query("delete from test_user where id = (:userId)")
fun deleteById(userId: Long)
}
新建一个DAO主要SQL语句的设置,可以从如下几个方面进行设置:
1. 插入数据
- 插入数据方法需要在方法前使用
@Insert
注解进行标识。 - 当参数只有一个时,则它可以返回 long,这是插入项的新 rowId。
- 如果参数是数组或集合,则应返回 long[] 或 List<Long>。
- 查看编译文件,可以发现插入操作是在一个独立的事务中完成的,保证了插入操作的原子性。
@Dao
interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Insert
fun insertBothUsers(user1: User, user2: User)
@Insert
fun insertUsersAndFriends(user: User, friends: List<User>)
}
2. Update
- Update方法需要采用注解
@Update
来完成 - Update方法使用和每个实体的主键匹配查询
- Update方法也可以返回Int值,表示数据库中更新的行数。
@Dao
interface MyDao {
@Update
fun updateUsers(vararg users: User)
}
3. 删除
- Delete方法需要采用注解
@Delete
进行标识 - Delete方法使用的是主键查询要删除的实体。
- Delete方法也可以返回Int值,表示数据库中更新的行数。
@Dao
interface MyDao {
@Delete
fun deleteUsers(vararg users: User)
}
4. 查询
- 查询方法需要使用
@Query
注解进行标识 -
@Query
不仅仅可以用来标识查询方法,还可以标识其他的任意SQL语句 - 每个
@Query
方法在编译时就是检查SQL语句是否正确,如果存在错误则直接编译报错,而不是运行时失败。
@Dao
interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
}
5. 带参数查询
- 在查询过程通添加条件不可避免的需要添加参数,可以在SQL语句中使用
:param
的方式引入参数。 - 如果编译检查SQL语句没有找打匹配的参数,则会编译报错。
@Dao
interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>
@Query("SELECT * FROM user WHERE first_name LIKE :search " +
"OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>
}
6. 传递参数的集合
- 存在有部分查询传入的数量不定的参数,参数的确切数量要到运行时才能得到,可以采用如下的参数合集的方式进行完成。
@Dao
interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}
7. 查询语句返回对象类型
- 如果需要返回实体类的部分字段,可以通过新建一个只包含这部分字段的类,返回返回此新类的集合对象。
- 查询语句还支持分布其他的返回类型,例如
RxJava中的Flowable<T>
等。 - DAO里的方法不能够放在主线程执行,因此需要等待
- 可以对DAO里的方法添加
suspend
关键字,以使用kotlin协程功能使得这个方法成为异步方法 - LiveData:如果返回为LiveData对象,则不需要进行异步操作,因为room会自动完成异步操作。
8. 事务
- 实现自定义事务需要添加如下注解
@Transaction
进行标识。
@Dao
abstract class UsersDao {
@Transaction
open suspend fun setLoggedInUser(loggedInUser: User) {
deleteUser(loggedInUser)
insertUser(loggedInUser)
}
@Query("DELETE FROM users")
abstract fun deleteUser(user: User)
@Insert
abstract suspend fun insertUser(user: User)
}
Entity
在Room DataBase中,Entity表示的是一张数据表的结构。举个例子来说,我们新建一个UserEntity类。
@Entity(tableName = "test_user")
class TestUserEntity(
@PrimaryKey
@ColumnInfo(name = "id")
var userId: Long,
@ColumnInfo(name = "name")
var name: String? = null
) {}
如上述代码所示,一个简单的user Entity类就定义好了,同时也定义了一个名为test_user
的数据表,其中的列信息主要有id和name两项,Entity对象和数据表的对应关系如下:
- 一个Entity对象代表数据表中的一行
- Entity类对应一个数据表,其成员变量对应数据表中的列
新建一个Entity可以从如下几个方面进行设置
1. 新建Entity
- 必须要使用
@Entity
注解来表示此类是一个Entity类。 - 可以在
@Entity
注解上使用属性tableName =
来指定此Entity对应的表的名字,如不采用则是使用默认命名。
2. 主键设置
- 每个Entity至少需要定义一个主键,即使此类只有一个属性,主键不能为空
- 对于主键字段可以使用
@PrimaryKey
注解来声明此字段为主键。 - 如果主键比较复杂,可以在
@Entity
注解中使用属性promaryKeys =
进行声明
@Entity(primaryKeys = {"id", "name"})
3. 列设置
- 使用
@ColumnInfo
来声明列信息,可以通过设置属性name =
来指定列的名称,如果不设置则会采用变量的小写形式作为列名称 - 如果在Entity类中存在一些变量不需要生成数据表的列,可以使用
@Ignore
注解注释需要被忽视的属性来不生成对应的列。 - 如果类继承上面有父类,并且不想父类的属性生成数据表中的列,可以采用
@ignoredColumns
。
//父类
open class User() {
var num: Long? = null
}
//子类定义为数据表结构,忽视
@Entity(tableName = "test_user", ignoredColumns = ["num"])
class TestUserEntity(
@PrimaryKey @ColumnInfo(name = "id") var userId: Long,
@ColumnInfo(name = "name") var name: String? = null
) : User() {
fun printData(): String {
Log.d("Message", Thread.currentThread().name)
return "id= $userId name= $name"
}
}
4. 嵌套Entity
如果定义的Entity对象里面有个ChildEntity类的对象,并且希望定义的Entity中表列表字段包括包含ChildEntity类对象中的变量,可以通过注解@Embedded
来实现,是来代码如下:
data class Address(
val street: String?,
val state: String?,
val city: String?,
@ColumnInfo(name = "post_code") val postCode: Int
)
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
@Embedded val address: Address?
)