WHAT
什么是数据存储?
缓存相关数据。当设备无法访问网络时,用户仍可在离线状态下浏览相应内容。设备重新连接到网络后,用户发起的所有内容更改都会同步到服务器。
常见的数据存储方式有:
- Shareprefence 存储
- 文件存储
- Sqlite存储
- 网络存储
什么是Room?
Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
WHY
为什么用Room做数据存储?
在 Android 中直接使用 SQLite 数据库存在多个缺点:
- 必须编写大量的样板代码;
- 必须为编写的每一个查询实现对象映射;
- 很难实施数据库迁移;
- 很难测试数据库;
- 如果不小心,很容易在主线程上执行长时间运行的操作。
为了解决这些问题,Google 创建了 Room,一个在 SQLite 上提供抽象层的持久存储库。
Room 是一个稳健的、基于对象关系映射(ORM)模型的、数据库框架。Room 提供了一套基于 SQLite 的抽象层,在完全实现 SQLite 全部功能的同时实现更强大的数据库访问。
针对 SQLite数据库的上述缺点,Room 框架具有如下特点:
- 由于使用了动态代理,减少了样板代码;
- 在 Room 框架中使用了编译时注解,在编译过程中就完成了对 SQL 的语法检验;
- 相对方便的数据库迁移;
- 方便的可测试性;
- 保持数据库的操作远离了主线程。
- 此外 Room 还支持 RxJava2 和 LiveData。
HOW
Room怎么做数据存储的?
添加依赖
apply plugin: 'kotlin-kapt'
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// 可选-支持协程
implementation "androidx.room:room-ktx:$room_version"
// 可选-测试
testImplementation "androidx.room:room-testing:$room_version"
}
一、简单使用
数据库表
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
DAO 提供数据增删改查
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
数据库
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
创建数据库实例
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
注意:如果您的应用在单个进程中运行,在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。
二、数据表
使用 Room 持久性库时,您可以将相关字段集定义为实体。对于每个实体,系统会在关联的 Database 对象中创建一个表,以存储这些项。您必须通过 Database 类中的 entities 数组引用实体类。
@Entity
data class User(
@PrimaryKey var id: Int,
var firstName: String?,
var lastName: String?
)
这里用到了几个注解,说明一下:
序号 | 注解名称 | 描述 |
---|---|---|
1 | @Entity | 声明所标记的类是一个数据表,@Entity 包括的参数有:tableName(表名),indices(表的索引),primaryKeys(主键),foreignKeys(外键),ignoredColumns(忽略实体中的属性,不作为数据表中的字段),inheritSuperIndices(是否集成父类的索引,默认 false) |
2 | @ColumnInfo | 用来声明数据库中的字段名 |
3 | @PrimaryKey | 被修饰的属性作为数据表的主键,@PrimaryKey 包含一个参数:autoGenerate(是否允许自动创建,默认false) |
4 | @Embedded | 用来修饰嵌套字段,被修饰的属性中的所有字段都会存在数据表中 |
主键
主键是数据唯一的索引,用来标识数据的唯一性
- 每个实体必须将至少 1 个字段定义为主键,且必须用@PrimaryKey注解
- 自动分配id 需要用@PrimaryKey的autoGenerate属性
- 复合主键使用@PrimaryKeys注解
//自动分配ID
@Entity
class User{
@PrimaryKey(autoGenerate = true) var id: Int = 0
override fun toString(): String {
return "id==$id"
}
}
//复合主键
@Entity(primaryKeys = arrayOf("firstName", "lastName"))
data class User(
val firstName: String?,
val lastName: String?
)
并不是每个表都存在ID字段,例如学生表(姓名,生日,性别,班级),这里面每一个值都可能重复,无法使用单一字段作为主键,这时我们可以将多个字段设置为复合主键,由复合主键标识唯一性。只要不是复合主键每个值都重复,就不算重复。
重命名表名
自定义Entity中tableName属性
@Entity(tableName = "users")
data class User (
// ...
)
重写列名
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
忽略字段
默认情况下,Room 会为实体中定义的每个字段创建一个列。如果某个实体中有您不想保留的字段,则可以使用 @Ignore 为这些字段添加注释,如以下代码段所示:
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val lastName: String?,
@Ignore val 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()
创建嵌套对象
由于 SQLite 是关系型数据库,因此您可以指定各个实体之间的关系。尽管大多数对象关系映射库都允许实体对象互相引用,但 Room 明确禁止这样做。如需了解此决策背后的技术原因,请参阅了解 Room 为何不允许对象引用。
如果实体中包含其他实体,例如Teacher实体包含Student实体,通常情况下将这些数据存储在数据库中有两种方式:
- 新建一张表Student表,用Teacher的id作为外键,与Teacher一一关联。
- 将Student中的字段,放在Teacher表中,这种方式减少了数据表的创建,也减少了联合查询的复杂程度。
如果直接将这些字段打散在 Player 表中,显得不够面向对象,这时就可以使用@Embedded注解,即显得面向对象,又不用再创建数据表,非常的优雅。
@Entity
data class Teacher(
@PrimaryKey val id:Int?,
@ColumnInfo val chinaName:String,
@ColumnInfo val enName:String,
@Embedded val stuBoy: Student,
@Embedded(prefix = "pre_") val stuGirl:Student
)
data class Student(
val stuId:Int
val name:String,
val clazz:String,
val age:Int,
val code:Long
)
表示 Teacher 对象的表将包含具有以下名称的列id,chinaName,enName,stuBoy,stuGirl,name,clazz,age,code
如果某个实体具有相同类型的多个嵌套字段,您可以通过设置 prefix 属性确保每个列的唯一性例如Teacher包含两个Student,stuGirl中的列名会加pre_前缀
@Insert
fun insert(teacher:Teacher)
@Query("SELECT * From Teacher WHERE pre_code = 1")
fun query()
表与表的关系
一对一关系
两个实体之间的一对一关系是指这样一种关系:父实体的每个实例都恰好对应于子实体的一个实例,反之亦然。
例如,假设有一个音乐在线播放应用,用户在该应用中具有一个属于自己的歌曲库。每个用户只有一个库,而且每个库恰好对应于一个用户。因此,User 实体和 Library 实体之间就应存在一种一对一的关系。
首先,为您的两个表分别创建一个类。其中一个表必须包含一个变量,且该变量是对另一个表体的主键的引用。
//主表
@Entity
data class User(
@PrimaryKey val userId:Int,
val name:String,
val age:Int
)
//子表
@Entity
data class Library(
@PrimaryKey val libraryId:Int,
val name:String,
//映射主表的主键userId
val userOwnId:Int
)
如果要查询所用用户和所有的库需要两次查询;
SELECT * FROM User
SELECT * FROM Library
WHERE userOwnId IN (ownerId1, ownerId2, …)
用了root后:
如需查询用户列表和对应的库,您必须先在两个实体之间建立一对一关系,创建一个新的数据类,其中每个实例都包含父表的一个实例和与之对应的子表实例。将 @Relation 注释添加到子表的实例,同时将 parentColumn 设置为父表主键列的名称,并将 entityColumn 设置为引用父表主键的子表列的名称。
/**
* 1对1关系 - 第三方实体类
*/
data class UserAndLibrary (
@Embedded val user:User,
//添加隐射关系 子表的userOwnId-父表的userId
@Relation(
parentColumn = "userId",
entityColumn = "userOwnId"
)
val library: Library
)
在Dao中添加方法
@Dao
interface UserAndLibraryDao {
//查询所有的user和library
@Transaction
@Query("SELECT * FROM User")
fun getUserList():List
//查询某个User和对应的library
@Transaction
@Query("SELECT * FROM user WHERE userId In (:ownId)")
fun getUser(ownId:Int):UserAndLibrary
@Insert
fun insertUser(user:User)
@Insert
fun insertLibrary(library:Library)
}
创建Db
@Database(entities = arrayOf(User::class,Library::class),version = 1)
abstract class UserAndLibraryDb:RoomDatabase() {
abstract fun getUserAndLibraryDao():UserAndLibraryDao
}
添加和查询结果
fun query(view: View) {
val result = db.getUserAndLibraryDao().getUser(1)
//查询对应的library的name
content.text = result.library.name
}
fun insert(view: View) {
val user = User(1,"XIAOMING",31)
val library = Library(123,"高碑店图书馆",1)
db.getUserAndLibraryDao().insertUser(user)
db.getUserAndLibraryDao().insertLibrary(library)
}
打印结果
高碑店图书馆
一对多关系
两个表之间的一对多关系是指这样一种关系:父表的每个实例对应于子表的零个或多个实例,但子表的每个实例只能恰好对应于父表的一个实例。
在音乐在线播放应用示例中,假设用户可以将其歌曲整理到播放列表中。每个用户可以创建任意数量的播放列表,但每个播放列表只能由一个用户创建。因此,User 表和 Playlist 表之间应存在一种一对多关系。
首先创建两个表
@Entity
data class User(
@PrimaryKey val userId: Long,
val name: String,
val age: Int
)
@Entity
data class Playlist(
@PrimaryKey val playlistId: Long,
val userCreatorId: Long,
val playlistName: String
)
创建一个新的数据类,其中每个实例都包含父表的一个实例和与之对应的所有子表实例的列表。将 @Relation 注释添加到子表的实例,同时将 parentColumn 设置为父表主键列的名称,并将 entityColumn 设置为引用父表主键的子表列的名称。
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
//注意此处是List
val playlists: List
)
最后在Dao中添加方法
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List
多对多关系
两个实体之间的多对多关系是指这样一种关系:父实体的每个实例对应于子实体的零个或多个实例,反之亦然。
在音乐在线播放应用示例中,再次考虑用户定义的播放列表。每个播放列表都可以包含多首歌曲,每首歌曲都可以包含在多个不同的播放列表中。因此,Playlist 实体和 Song 实体之间应存在多对多的关系。
首先,为您的两个实体分别创建一个类。多对多关系与其他关系类型均不同的一点在于,子实体中通常不存在对父实体的引用。因此,需要创建第三个类来表示两个实体之间的关联实体(即交叉引用表)。交叉引用表中必须包含表中表示的多对多关系中每个实体的主键列。在本例中,交叉引用表中的每一行都对应于 Playlist 实例和 Song 实例的配对,其中引用的歌曲包含在引用的播放列表中。
@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
)
下一步取决于您想如何查询这些相关实体。
如果您想查询播放列表和每个播放列表所含歌曲的列表,则应创建一个新的数据类,其中包含单个 Playlist 对象,以及该播放列表所包含的所有 Song 对象的列表。
如果您想查询歌曲和每首歌曲所在播放列表的列表,则应创建一个新的数据类,其中包含单个 Song 对象,以及包含该歌曲的所有 Playlist 对象的列表。
在这两种情况下,都可以通过以下方法在实体之间建立关系:在上述每个类中的 @Relation 注释中使用 associateBy 属性来确定提供 Playlist 实体与 Song 实体之间关系的交叉引用实体。
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = @Junction(PlaylistSongCrossRef::class)
)
val songs: List
)
data class SongWithPlaylists(
@Embedded val song: Song,
@Relation(
parentColumn = "songId",
entityColumn = "playlistId",
associateBy = @Junction(PlaylistSongCrossRef::class)
)
val playlists: List
)
最后,向 DAO 类添加一个方法,用于提供您的应用所需的查询功能。
- getPlaylistsWithSongs:该方法会查询数据库并返回查询到的所有 PlaylistWithSongs 对象。
- getSongsWithPlaylists:该方法会查询数据库并返回查询到的所有 SongWithPlaylists 对象。
这两个方法都需要 Room 运行两次查询,因此应为这两个方法添加 @Transaction 注释,以确保整个操作以原子方式执行。
@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List
@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List
Room怎么配合其他组件使用的?
常见问题
问题一:更新实体,数据库版本没有升级
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.