在上一篇文章 一文理解Jetpack——SQLite 中,我们介绍了 SQLite
相关的知识。但是 Google 是不推荐我们直接使用 SQLite
的,而是推荐使用 Room
库来操作数据库。
要使用 Room 库我们需要先引入依赖,代码如下:
// 引入依赖
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
Room 的组成
Room
主要包含三个组件,分别为 DataBase
、Entity
和 DAO
。其中 DataBase
是数据库持有者;Entity
表示数据库中对应的表;DAO
则提供了访问数据库方法的对象。在 Android 开发中,我们一般通过 DataBase
来获取对应数据库的访问对象 DAO
,然后使用 DAO
对 Entity
进行各种操作。它们之间的关系如下图所示,图片来源官方文档
代码示例如下:
// 通过 @Entity 在数据库中定义了 user 表
// User 类的每个实例都代表应用数据库中 user 表中的一行
@Entity
data class User(
@PrimaryKey val uid: Int, // @PrimaryKey 定义主键,必须要有一个
// @ColumnInfo 设置对应表的字段名
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// 通过 UserDao 对象来操作 user 表
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
}
// entities 数组表示与数据库关联的数据实体;version 表示数据库版本号
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// 定义数据实体、DAO 和数据库对象后
// 我们就可以使用以下代码创建数据库实例并与数据库进行交互了
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
使用 @Entity 定义表
从上面的代码示例可以看到,Room
通过 @Entity
注解来定义数据库中的表。@Entity
的定义如下所示,可以看到 @Entity
内部有很多字段,接下来我们一一介绍。
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
public annotation class Entity(
// 数据表名, 默认情况下以类名为表名
val tableName: String = "",
// 索引
val indices: Array<Index> = [],
// 是否继承父类索引
val inheritSuperIndices: Boolean = false,
// 复合主键
val primaryKeys: Array<String> = [],
// 外键约束
val foreignKeys: Array<ForeignKey> = [],
// 忽略字段数组
val ignoredColumns: Array<String> = []
)
tableName
默认情况下,Room 会以类名为表名。我们也可以使用 tableName 来主动设置表的名字。代码示例如下:
@Entity(tableName = "user_info")
data class User(
...
)
indices
indices
用来定义索引。索引是一种特殊的查找表。通过索引,数据库可以加快数据的SELECT 查询过程。在 Room 中,索引的定义非常简单,只需要使用 indices
来声明就可以了。代码示例如下:
@Entity(indices = [Index(value = ["last_name", "address"])])
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val address: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
)
声明索引之后,数据库内部会完成索引的处理,后面我们只需要正常的查询就可以了,不需要关心内部的实现。唯一需要注意的是,索引虽然会加快数据的SELECT 查询过程,但是会减慢使用 UPDATE 和 INSERT 的数据更新和插入过程。因此创建索引时,需要考虑以下几点:
- 较小的表上,不建议使用索引
- 声明索引的列,不应该频繁更新
- 需要频繁的更新或插入操作的表上,不建议使用索引
- 索引不应该使用在含有大量的 NULL 值的列上。
inheritSuperIndices
inheritSuperIndices
用来表示是否继承父类索引
primaryKeys
如下代码所示,一般情况下,我们使用 @PrimaryKey
来声明主键。但是如果我们需要通过多个列的组合对实体实例进行唯一标识。这时候我们就可以使用 primaryKeys
来定义一个复合主键。
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
primaryKeys
的使用如下:
@Entity(primaryKeys = ["firstName", "lastName"])
data class User(
val firstName: String?,
val lastName: String?
)
foreignKeys
foreignKeys
用来做外键约束。代码示例如下:
// 学生表
@Entity
data class Student(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String
)
// 成绩单表
@Entity(tableName = "grade_result")
data class GradeResult(
@PrimaryKey(autoGenerate = true) val gradeId: Long = 0,
@ColumnInfo(name = "student_id") val studentId: Long
)
可以看到, 成绩单表通过 student_id
关联到学生表。因此 student_id
是成绩单表的外键。但是这里有一个问题,就是我们可以随便给成绩单表添加任何数据,即使 student_id
在 学生表 中找不到。
这时候,就可以使用外键约束。有了外键约束后,当我们插入数据时,如果没有对应的student_id
则会报错。创建外键约束的代码如下:
@Entity(tableName = "grade_result",
foreignKeys = [ForeignKey(
entity = Student::class,
childColumns = ["student_id"],
parentColumns = ["id"]
)])
data class GradeResult(
@PrimaryKey(autoGenerate = true) val gradeId: Long = 0,
@ColumnInfo(name = "student_id") val studentId: Long
)
ignoredColumns
ignoredColumns
表示忽略字段的数组。一般情况下,我们使用 @Ignore
来忽略该字段。代码示例如下:
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val lastName: String?,
@Ignore val picture: Bitmap?
)
但是如果需要忽略父类的字段,这时就需要ignoredColumns
了,代码示例如下:
open class User {
var picture: Bitmap? = null
}
@Entity(ignoredColumns = ["picture"])
data class RemoteUser(
@PrimaryKey val id: Int,
val hasVpn: Boolean
) : User()
复杂数据类型转换
在 Room 中,不允许实体类之间的引用。具体原因可以看了解 Room 为何不允许对象引用。因此我们需要使用类型转化器来支持自定义类型。代码示例如下:
// 比如说,我们需要存储 Date 信息,就可以定义 Date 的转化器
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
// 使用 @TypeConverters 声明转化器
@TypeConverters(Converters::class)
@Entity
data class User(private val birthday: Date?)
声明嵌套关系
有的时候,我们需要需要声明嵌套关系。比如说用户的信息包含名字、地址等,而地址信息又包含城市、街道等。代码示例如下,我们可以使用 @Embedded
注解来声明嵌套关系。
data class Address(
val street: String?,
val city: String?,
)
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String?,
@Embedded val address: Address?
)
在 User
表中,address
会以 city
、street
字段的形式存储在表中。如果你想让 Address
存储在不同的表中,可以看后面的表之间的关系。
如何操作数据
在 Room
中,我们通过 DAO
来操作数据。代码示例如下:
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Query("SELECT * FROM user")
fun getAll(): List<User>
}
@Insert
在 DAO
中,使用 @Insert
来标注插入数据的方法(可以插入一条数据或者多条)。其中有一个onConflict
参数,是用于定义当插入数据冲突时执行的操作。它有六种处理方式,其中2种已经过时了。其中未过时的4种处理方式如下所示:
策略 | 作用 |
---|---|
OnConflictStrategy.ABORT | 终止插入,并且抛出异常 |
OnConflictStrategy.NONE | 默认的策略,它和ABORT 作用是一样的 |
OnConflictStrategy.REPLACE | 覆盖原数据 |
OnConflictStrategy.IGNORE | 忽略这条数据,若是有返回值的话则返回-1 |
@Delete
在 DAO
中,使用 @Delete
来标注删除数据的方法(可以删除一条数据或者多条)。代码示例如下:
@Delete
fun deleteUsers(vararg users: User)
@Update
在 DAO
中,使用 @Update
来标注更新数据的方法(可以更新一条数据或者多条)。代码示例如下:
@Update
fun updateUsers(vararg users: User)
@Query
在 DAO
中,使用 @Query
来标注查询数据的方法。代码示例如下:
// 查询表内所有的数据
@Query("SELECT * FROM user")
fun getAll(): List<User>
// 查询表内的部分字段
data class NameTuple(
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// 条件查询
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
@Transaction
在 DAO
中,如果我们想要多个操作在一个事务中执行,我们可以使用 @Transaction
注解。代码示例如下:
// 先后插入学生和成绩信息,以事务的方式执行
@Transaction
fun insertStudentAndGrade(student: Student, grade: Grade) {
insertStudent(student)
insertGrade(grade)
}
复杂的SQL操作
如果你的 SQL 非常复杂。只使用上面 Room 提供的注解无法完成对应的操作。这时你可以通过 RoomDatabase
获取 SQLiteOpenHelper
对象来直接执行对应的 SQL 方法。代码示例如下:
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
db.openHelper.writableDatabase.execSQL("你的sql")
异步操作
需要注意,上面的操作都是同步的。如果需要异步操作,我们只需要给对应的 DAO
方法加上 suspend
就可以了。代码示例如下:
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(vararg users: User)
@Delete
suspend fun deleteUsers(vararg users: User)
@Update
suspend fun updateUsers(vararg users: User)
@Query("SELECT * FROM user")
suspend fun getAll(): List<User>
}
如果你需要监听操作执行的结果,可以使用 Flow
或者 LiveData
。代码示例如下:
// 使用 Flow
@Query("SELECT * FROM user")
suspend fun getAll(): Flow<List<User>>
// 使用 LiveData
@Query("SELECT * FROM user")
fun getAll(): LiveData<List<User>>
注意,使用 Flow 或者 LiveData 监听数据时,需要使用
distinctUntilChanged
去重。这是因为,表执行了任何更新,都会通知过来,无论这个更新的字段你是否关心。
表之间的关系
上面的查询操作中,我们都是对一个表进行操作。虽然多数情况下,使用一个表存储数据就够了。但是有时候,可能需要创建多个表。这时候就需要定义表之间的关系了。
表之间的关系一般有四种,分别是一对一、一对多、多对多、嵌套关系。下面分别介绍如何定义这些关系。
一对一关系
在 Room 中使用 @Relation
注解来建立两个实例之间的关系。代码示例如下:
@Entity
data class User(
@PrimaryKey val userId: Long,
val name: String,
val age: Int
)
@Entity
data class Library(
@PrimaryKey val libraryId: Long,
val userOwnerId: Long
)
data class UserAndLibrary(
@Embedded val user: User, // 使用 @Embedded 映射到 User 表对应的字段
@Relation(
parentColumn = "userId", // User 主键
entityColumn = "userOwnerId" // 外键
)
val library: Library
)
之后我们我们就可以在 DAO
中来查询了,代码示例如下:
// 由于内部实现会涉及到 Room 两次查询,因此需要使用 @Transaction 。下面的关系也是一样的
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
一对多关系
一对多关系和一对一关系的声明是一样的,不同的是对应的结果是集合。代码示例如下:
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val librarys: List<Library> // 不同点
)
多对多关系
多个歌单和多个歌曲形成了多对多的关系。我们需要一个额外的表 PlaylistSongCrossRef
来表明它们的引用关系。代码示例如下:
// 播放列表
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
val playlistName: String
)
// 歌曲信息
@Entity
data class Song(
@PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
// 歌单和歌曲的引用表
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
// 歌单及歌单中对应的歌曲
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class) // 不同点:通过 associateBy 来设置引用表
)
val songs: List<Song>
)
嵌套关系
假设一个音乐应用,我们需要获取一个用户的所有歌单、以及对应的歌曲信息。这时就需要嵌套查询,代码示例如下:
data class UserWithPlaylistsAndSongs(
@Embedded val user: User // 用户信息
@Relation(
entity = Playlist::class, // 不同点:User 与 Playlist 对应,因此这里需要设置为 Playlist::class
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs>
)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
val userCreatorId: Long,
val playlistName: String
)
@Entity
data class Song(
@PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
嵌套关系如下图所示:
数据库升级
当我们需要删除或者重命名表和字段时,需要升级数据库的版本。在 Room 中,我们可以通过 @Database
的 autoMigrations
属性来实现。代码示例如下:
@Database(
version = 2,
entities = [User::class],
autoMigrations = [
AutoMigration (
from = 1,
to = 2,
spec = AppDatabase.MyAutoMigration::class
)
]
)
abstract class AppDatabase : RoomDatabase() {
// 重命名表名
@RenameTable(fromTableName = "User", toTableName = "AppUser")
class RenameTableAutoMigration : AutoMigrationSpec
// 删除表
@DeleteTable(tableName = "User")
class DeleteTableAutoMigration : AutoMigrationSpec
// 重命名字段
@RenameColumn(tableName = "User", fromColumnName = "id", toColumnName = "user_id")
class RenameColumnAutoMigrationSpec: AutoMigrationSpec
// 删除字段
@DeleteColumn(tableName = "User", columnName = "nick_name")
class DeleteColumnAutoMigrationSpec: AutoMigrationSpec
}
如果你想完成更复杂的更新逻辑,也可以手动升级。代码示例如下:
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()