Room是基于SQLite上,提供的一个抽象层。以便在充分利用SQLite 的强大功能的同时,能够更加强健的去访问数据库。 Room作用是可以用于缓存数据。当设备无法访问网络时,用户仍可在离线状态下浏览相应内容。设备重新连接到网络后,用户发起的所有内容更改都会同步到服务器。Google强烈推荐使用Room,而不直接使用SQLite
一:添加依赖
在app的build.gradle上添加如下代码
def room_version = "2.2.6"
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"
复制代码
二:Room结构
Room包含3个主要组件:
- DataBase:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。简单例子如下:
使用@Database注释的类应注意什么:@Database(entities = arrayOf(User::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } 复制代码
- 继承RoomDatabase的抽象类 abstract class AppDatabase : RoomDatabase()
- 在@Database数据中,添加于数据库关联的实体列表。 @Database(entities = arrayOf(User::class), version = 1)
- 包含具有0个参数切返回使用@Dao 注释的类的抽象方法。abstract fun userDao(): UserDao
- Entity:表示数据库中的表,简单例子如下:
@Entity data class User( @PrimaryKey var uid: Int, @ColumnInfo(name = "first_name") var firstName: String? ) 复制代码
- DAO:包含用于访问数据库的方法。简单例子如下:
@Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> } 复制代码
Room 不同组件之间的关系如图所示:
三:Room简单使用实例
有个User类
@Entity
data class User(
@PrimaryKey(autoGenerate = true)
var uid:Int=1,var name:String?=""):Serializable
复制代码
- User里有两个属性一个是uid,作为主键,还有一个name属性
有个UserDao接口
@Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAll():List<User>
@Query("SELECT * FROM user WHERE uid in (:userIds)" )
suspend fun loadAllByIds(userIds:IntArray):List<User>
@Query("SELECT * FROM user WHERE name LIKE :userName LIMIT 1")
suspend fun findByName(userName:String):User
@Query("SELECT * FROM user WHERE uid = :uid")
suspend fun findByUid(uid:Int):User
// 插入一条,返回的是插入的那条的rowId,插入多条返回的是数组
@Insert
suspend fun insertAll(vararg users:User)
// 可以返回int,返回删除的行数
@Delete
suspend fun delete(user:User)
// 可以返回int,返回更新的行数
@Update
suspend fun updateUser(user:User)
}
复制代码
- UserDao定义了一些数据库增删改查的方法。subspend表示是这些数据库操作的方法需要在协程中运行。
有个AppDataBase抽象类
@Database(entities = arrayOf(User::class),version = 1)
abstract class AppDataBase :RoomDatabase(){
abstract fun userData():UserDao
}
复制代码
- 该类继承RoomDatabase,并且声明了entities为User,数据库为1,并且声明了抽象接口返回UserDao
有个RoomManager,管理类
object RoomManager {
private var db: RoomDatabase? = null
fun getDB(context: Application): AppDataBase{
if (db == null) {
db = Room.databaseBuilder(context, AppDataBase::class.java, "ccm_db").enableMultiInstanceInvalidation().build()
}
return db as AppDataBase
}
}
复制代码
- RoomManager为单例,声明了getDB方法,传入application去生成AppDataBase实例。\
注意:如果您的应用在单个进程中运行,在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。
如果您的应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个 AppDatabase 实例,可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中 AppDatabase 的实例。\
- 默认情况下Room是不支持在主线程中访问数据库,如果要在主线程中访问,可以在构造器加上 allowMainThreadQueries()方法
但最好是不要在主线程中访问数据库,因为有可能因为耗时而导致ANR。db = Room.databaseBuilder(context, AppDataBase::class.java, "ccm_db").allowMainThreadQueries().build() 复制代码
有个RoomViewModel类
class RoomViewModel(val context:Application) :AndroidViewModel(context){
var users = MutableLiveData<List<User>>()
fun queryUsers(){
viewModelScope.launch{
val list = withContext(Dispatchers.IO){
RoomManager.getDB(context).userData().getAll()
}
users.value = list
}
}
fun insertUser(user:User){
viewModelScope.launch {
withContext(Dispatchers.IO){
RoomManager.getDB(context).userData().insertAll(user)
}
}
}
}
复制代码
- 在RoomViewModel去查询数据库数据,由于我们RoomManager去生成AppDataBase的时候,需要Application类,所以我们RoomViewModel需要继承AndroidViewModel
- 里面有个LiveData去存储user
- 定义了两个方法,一个是queryUsers。查询数据库的User列表
- 一个是insertUser 插入user数据库
- viewModelScope使用协程去处理数据库请求
接着是Activity的使用
class RoomTestActivity :AppCompatActivity(),View.OnClickListener{
lateinit var viewModel:RoomViewModel
var index = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_roomtest)
viewModel = ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(RoomViewModel::class.java)
viewModel.users.observe(this,
Observer<List<User>> {
tv_name.text = if(it?.size?:0>0) it[0].name else "名字"
})
btn_add.setOnClickListener(this)
btn_query.setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v){
btn_add->{
viewModel.insertUser(User(index,"ccm"))
index++
}
btn_query->{
viewModel.queryUsers()
}
}
}
}
复制代码
- Activity中有三个控件,一个插入按钮btn_add,一个查询按钮btn_query,一个文本按钮去显示第一条user数据
- viewModel=ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(RoomViewModel::class.java) 通过ViewModelProvider去创建对应的ViewModel
- viewModel.users.observe 通过观察数据变化的时候,去把对应user数据显示到文本控件上面
一个activity_roomtest.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="名字"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加user"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_name" />
<Button
android:id="@+id/btn_query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查询user"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
复制代码
以上就是Room简单的使用例子,不了解ViewModel跟LiveData用法的可以去看系列文章。
四:使用Room entities定义数据
使用 Room 时,我们可以通过注解@Entity定义实体类,每个实体类会对应一张表,我们可以通过 Database 类中的 entities 数组引用实体类。如下
// entities = arrayOf(User::class)引用User实体类
@Database(entities = arrayOf(User::class),version = 1)
abstract class AppDataBase :RoomDatabase(){
abstract fun userData():UserDao
}
// User实体类
@Entity
data class User(
@PrimaryKey(autoGenerate = true)
var uid:Int=1,var name:String?=""):Serializable
复制代码
4.1 使用主键
可以通过@PrimaryKey定义主键,并可以通过autoGenerate指定是否为自增长:
@Entity
data class User(
@PrimaryKey(autoGenerate = true)
var uid:Int=1,var name:String?=""):Serializable
复制代码
也可以通过@Entity的primaryKeys值去定义复合主键
// 定义复合主键
@Entity(primaryKeys = arrayOf("firstName", "lastName"))
data class User(var firstName: String?, var lastName: String?)
复制代码
4.2 自定义表名
默认情况下,Room会将类名作为表名,我们可以通过@Entity的tableName属性去定义表的名字。
@Entity(tableName = "users")
data class User (
// ...
)
复制代码
4.2 自定义列名
默认情况下,Room会将属性名作为列名,我们可以通过@ColumnInfo去自定义列名。
@Entity(tableName = "users")
data class User (
@PrimaryKey var id: Int,
@ColumnInfo(name = "first_name") var firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?
)
复制代码
4.3 忽略字段
默认情况下,Room会为实体类中的每个字段都生成对应的表的列。如果我们有某个字段不想在建列,那么可以使用@Ignore注解,表示忽略该字段
@Entity
data class User(
@PrimaryKey var id: Int,
var firstName: String?,
var lastName: String?,
@Ignore var picture: Bitmap?
)
复制代码
如果实体类继承了父类的字段,想要忽略父类的字段可以使用@Entity的ignoredColumns字段去声明
open class User {
var picture: Bitmap? = null
}
@Entity(ignoredColumns = arrayOf("picture"))
data class RemoteUser(
@PrimaryKey val id: Int,
val hasVpn: Boolean
) : User()
复制代码
五:使用Room DAO 访问数据
DAO 可以定义一些操作数据库的方法。
5.1 Insert
@Insert是往数据库中插入数据
@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>)
}
复制代码
如果 @Insert 方法只接收 1 个参数,则它可以返回 long,是插入项的rowId。如果参数是数组或集合,则应返回 long[] 或 List<Long>。
5.2 Update
@Update是更新数据库的内容
@Dao
interface MyDao {
@Update
fun updateUsers(vararg users: User)
}
复制代码
虽然通常没有必要,但是您可以让此方法返回一个 int 值,以指示数据库中更新的行数
5.3 Delete
从数据库中删除数据
@Dao
interface MyDao {
@Delete
fun deleteUsers(vararg users: User)
}
复制代码
虽然通常没有必要,但是您可以让此方法返回一个 int 值,以指示从数据库中删除的行数。
5.4 Query
从数据库中查询数据
@Dao
interface MyDao {
@Query("SELECT * FROM user")
fun loadAllUsers(): List<User>
}
复制代码
条件查询,比如查询你年龄小于minAge的用户
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE uid = :uid")
suspend fun findByUid(uid:Int):User
}
复制代码
大多数情况下,您只需获取实体的几个字段。例如,您的界面可能仅显示用户的名字和姓氏,而不是用户的每一条详细信息,那么我们可以查询指定的字段,来提交查询效率
data class NameTuple(
@ColumnInfo(name = "first_name") var firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?
)
@Dao
interface MyDao {
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
}
复制代码
您的部分查询可能要求您传入数量不定的参数,参数的确切数量要到运行时才知道。例如,您可能希望从部分区域中检索所有用户的相关信息。这时可以用in
@Dao
interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}
复制代码
多表查询
@Dao
interface MyDao {
@Query(
"SELECT * FROM book " +
"INNER JOIN loan ON loan.book_id = book.id " +
"INNER JOIN user ON user.id = loan.user_id " +
"WHERE user.name LIKE :userName"
)
fun findBooksBorrowedByNameSync(userName: String): List<Book>
}
复制代码
5.5 查询返回类型
Room 支持各种查询方法的返回类型,包括与特定框架或 API 进行互操作的特殊返回类型。下表根据查询类型和框架展示了适用的返回类型:
查询类型 | 协程 | RxJava | Guava | 生命周期 |
---|---|---|---|---|
可观察读取 | Flow<T> | Flowable<T> Publisher<T> Observable<T> | 无 | LiveData<T> |
单次读取 | suspend fun | Single<T>、Maybe<T> | ListenableFuture<T> | 无 |
单次写入 | suspend fun | Single<T>、Maybe<T>、Completable<T> | ListenableFuture<T> | 无 |
5.5.1 使用流进行响应式查询
在 Room 2.2 及更高版本中,您可以使用 Kotlin 的 Flow 功能确保应用的界面保持最新状态。如需在基础数据发生变化时使界面自动更新,请编写返回 Flow 对象的查询方法:
@Query("SELECT * FROM User")
fun getAllUsers(): Flow<List<User>>
复制代码
只要表中的任何数据发生变化,返回的 Flow 对象就会再次触发查询并重新发出整个结果集。
使用 Flow 的响应式查询有一个重要限制:只要对表中的任何行进行更新(无论该行是否在结果集中),Flow 对象就会重新运行查询。通过将 distinctUntilChanged() 运算符应用于返回的 Flow 对象,可以确保仅在实际查询结果发生更改时通知界面:
@Dao
abstract class UsersDao {
@Query("SELECT * FROM User WHERE username = :username")
abstract fun getUser(username: String): Flow<User>
fun getUserDistinctUntilChanged(username:String)=getUser(username).distinctUntilChanged()
}
复制代码
注意:如需将 Room 与 Flow 一起使用,您需要在 build.gradle 文件中包含 room-ktx 工件。如需了解详情,请参阅声明依赖项。
5.5.2 使用 Kotlin 协程进行异步查询
您可以将 suspend Kotlin 关键字添加到 DAO 方法中,以使用 Kotlin 协程功能使这些方法成为异步方法。这样可确保不会在主线程上执行这些方法。
@Dao
interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(vararg users: User)
@Update
suspend fun updateUsers(vararg users: User)
@Delete
suspend fun deleteUsers(vararg users: User)
@Query("SELECT * FROM user")
suspend fun loadAllUsers(): Array<User>
}
复制代码
注意:如需将 Room 与 Kotlin 协程一起使用,您需要使用 Room 2.1.0、Kotlin 1.3.0 和 Cordoines 1.0.0 或更高版本。如需了解详情,请参阅声明依赖项。
suspend 关键字同时适用于带有 @Transaction 注释的 DAO 方法,这些方法会在单个数据库事务中运行。
@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)
}
复制代码
5.5.3 使用 LiveData 进行可观察查询
执行查询时,可以通过使用 LiveData 类型的返回值,确保应用的界面在数据发生变化时自动更新,当数据库更新时,Room 会生成更新 LiveData 所必需的所有代码。
@Dao
interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}
复制代码
5.5.4 使用 RxJava 进行响应式查询
Room 为 RxJava2 类型的返回值提供了以下支持:
- @Query 方法:Room 支持 Publisher、Flowable 和 Observable 类型的返回值。
- @Insert、@Update 和 @Delete 方法:Room 2.1.0 及更高版本支持 Completable、Single<T> 和 Maybe<T> 类型的返回值。
如需使用rxjava跟room一起使用,那么需要app/build.gradle引入
def room_version = "2.1.0"
implementation 'androidx.room:room-rxjava2:$room_version'
复制代码
Rxjava跟Room使用例子:
@Dao
interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
fun loadUserById(id: Int): Flowable<User>
// Emits the number of users added to the database.
@Insert
fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>
// Makes sure that the operation finishes successfully.
@Insert
fun insertLargeNumberOfUsers(varargs users: User): Completable
/* Emits the number of users removed from the database. Always emits at
least one user. */
@Delete
fun deleteAllUsers(users: List<User>): Single<Int>
}
复制代码
六:定义对象之间的关系
由于 SQLite 是关系型数据库,因此我们可以指定各个实体之间的关系
6.1:创建嵌套对象
我们经常会有类里面包含某个类的情况,比如 User 类可以包含一个 Address 类型的字段,Address有city,street属性。我们希望User里包含Address类,直接把Address里的属性作为User的列。那么可以使用@Embedde注解。如下:
data class Address( var street: String?, var city: String? )
@Entity
data class User(
@PrimaryKey var id: Int,
var firstName: String?,
@Embedded var address: Address?
)
复制代码
表示 User 对象的表将包含具有以下名称的列:id、firstName、street、city。
6.2:定义一对一关系
两个实体之间的一对一关系是指这样一种关系:父实体的每个实例都恰好对应于子实体的一个实例,反之亦然。 例如,每个用户只有一个歌曲库,而且每个歌曲库恰好对应于一个用户。因此,User 实体和 Library 实体之间就应存在一种一对一的关系。
@Entity
data class User(
@PrimaryKey var userId: Long,
var name: String,
var age: Int
)
@Entity
data class Library(
@PrimaryKey var libraryId: Long,
var userOwnerId: Long
)
复制代码
如需查询用户列表和对应的歌曲库,您必须先在两个实体之间建立一对一关系。为此,请创建一个新的数据类UserAndLibrary
data class UserAndLibrary(
@Embedded var user: User?,
@Relation(parentColumn = "userId",entityColumn = "userOwnerId")
var library: Library?
)
复制代码
- @Relation 注释添加到实例Library上, parentColumn 为User的主键名称,entityColumn为user的主键在Library里的列名。
最后需要在Dao类添加一个方法,用于返回User跟Library配对的数据类的所有实例。由于该方法需要Room运行两次查询,因此应向该方法添加 @Transaction 注释,以确保整个操作以原子方式执行
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
复制代码
6.3:定义一对多关系
两个实体之间的一对多关系是指这样一种关系:父实体的每个实例对应于子实体的零个或多个实例,但子实体的每个实例只能恰好对应于父实体的一个实例。 例如,每个用户有可以创建很多个播放列表,而且每个播放列表都只属于一个用户。因此,User 实体和 PlayList 实体之间就应存在一种一对多的关系。
@Entity
data class User(
@PrimaryKey var userId: Long,
var name: String,
var age: Int
)
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
var userCreatorId: Long,
var playListName:String
)
复制代码
为了查询用户列表和对应的播放列表,您必须先在两个实体之间建立一对多关系,为此,请创建一个新的数据类UserWithPlaylists,包含一个父实体User,和一个子实体列表List<PlayList>
data class UserWithPlaylists(
@Embedded var user: User?,
@Relation(parentColumn = "userId",entityColumn = "userCreatorId")
var playlists: List<Playlist>?
)
复制代码
- @Relation 注释添加到子实体列表playlists上, parentColumn 为父实体User的主键名称,entityColumn为user的主键在子实体Playlist里的列名。
最后,向 DAO 类添加一个方法,用于返回将父实体与子实体配对的数据类的所有实例。该方法需要 Room 运行两次查询,因此应向该方法添加 @Transaction 注释,以确保整个操作以原子方式执行。
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>
复制代码
6.4:定义多对多关系
两个实体之间的多对多关系是指这样一种关系:父实体的每个实例对应于子实体的零个或多个实例,反之亦然。
举例:每个播放列表都可以包含多首歌曲,每首歌曲都可以包含在多个不同的播放列表中。因此,Playlist 实体和 Song 实体之间应存在多对多的关系
首先,为您的两个实体分别创建一个类。多对多关系与其他关系类型均不同的一点在于,子实体中通常不存在对父实体的引用。因此,需要创建第三个类来表示两个实体之间的关联实体(即交叉引用表)。交叉引用表中必须包含表中表示的多对多关系中每个实体的主键列
@Entity
data class Playlist(
@PrimaryKey var playlistId: Long,
var playlistName: String
)
@Entity
data class Song(
@PrimaryKey var songId: Long,
var songName: String,
var artist: String
)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
var playlistId: Long,
var songId: Long
)
复制代码
下一步取决于您想如何查询这些相关实体。
- 如果您想查询播放列表和每个播放列表所含歌曲的列表,则应创建一个新的数据类,其中包含单个 Playlist 对象,以及该播放列表所包含的所有 Song 对象的列表。
data class PlaylistWithSongs( @Embedded val playlist: Playlist?, @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = @Junction(PlaylistSongCrossRef::class) ) val songs: List<Song>? ) 复制代码
- 如果您想查询歌曲和每首歌曲所在播放列表的列表,则应创建一个新的数据类,其中包含单个 Song 对象,以及包含该歌曲的所有 Playlist 对象的列表。
data class SongWithPlaylists( @Embedded val song: Song, @Relation( parentColumn = "songId", entityColumn = "playlistId", associateBy = @Junction(PlaylistSongCrossRef::class) ) val playlists: List<Playlist> ) 复制代码
最后,向 DAO 类添加一个方法,用于提供您的应用所需的查询功能
- getPlaylistsWithSongs:该方法会查询数据库并返回查询到的所有 PlaylistWithSongs 对象。
- getSongsWithPlaylists:该方法会查询数据库并返回查询到的所有 SongWithPlaylists 对象。
这两个方法都需要 Room 运行两次查询,因此应为这两个方法添加 @Transaction 注释,以确保整个操作以原子方式执行。
@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>
@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>
复制代码
注意:如果 @Relation 注释不适用于您的特定用例,您可能需要在 SQL 查询中使用 JOIN 关键字来手动定义适当的关系。