Jetpack Room

·  阅读 1612

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(entities = arrayOf(User::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
          abstract fun userDao(): UserDao
    }
    复制代码
    使用@Database注释的类应注意什么:
    • 继承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 不同组件之间的关系如图所示: blockchain

三: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()方法
    db = Room.databaseBuilder(context, AppDataBase::class.java, "ccm_db").allowMainThreadQueries().build()
    复制代码
    但最好是不要在主线程中访问数据库,因为有可能因为耗时而导致ANR。

有个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简单的使用例子,不了解ViewModelLiveData用法的可以去看系列文章。

四:使用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 进行互操作的特殊返回类型。下表根据查询类型和框架展示了适用的返回类型:

查询类型协程RxJavaGuava生命周期
可观察读取Flow<T>Flowable<T> Publisher<T> Observable<T>LiveData<T>
单次读取suspend funSingle<T>、Maybe<T>ListenableFuture<T>
单次写入suspend funSingle<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 关键字来手动定义适当的关系。

分类:
Android
分类:
Android
收藏成功!
已添加到「」, 点击更改