Room 是Google官方在Jetpack中提供的基于SQLite的抽象层。有几个优点:
- 编译期 SQL 语法检查
- 设计友好,方便使用
- 支持
RxJava、LiveData、Coroutine
因此是目前基于SQLite的数据库管理的不二之选。
接入
dependencies {
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
...
}
基本组件
Room的三个核心组件:
Database: 操作数据库的入口Entity:数据表DataAccessObject (DAO): 读写数据层
定义 Entities
一个 Entity 代表一张表,使用@Entiry注解一个 class,这个Entity就会参与到表(Table)的创建,其属性对应表的column:
import android.graphics.Bitmap
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity
data class Song(
@PrimaryKey
val id: Long,
val name: String,
val artist: String,
val duration: Long
)
@Entity
data class Album(
@PrimaryKey
val id: Long,
val name: String,
@ColumnInfo(name = "front_cover")
val img: String,
@Ignore
var image: Bitmap?
)
-
所有字段都需要有
getter和setter且public -
使用
@ColumnInfo(name = "xxx")注解来定义对应属性在 Table 中的ColumnName。 -
至少有一个使用
@PrimaryKey修饰的主键。可以定义多个主键,多个主键的声明方式略有不同:@Entity(primaryKeys = ["id", "name"]) -
默认使用类名作为
TableName, 也可以通过tableName参数指定@Entity(tableName = "music") -
为
@Entity注解添加indices参数可指定数据库索引@Entity(indices = [Index("id", "name")]) @Entity(indices = [Index(value = ["id", "name"], unique = true)]) -
使用
@Ignore注解的属性不会作为 column 进行持久化
定义 Database
通过 @Database() 注解,继承自 RoomDatabase,声明数据库版本。关联所有可操作的表,提供获取 DAO 的无参数抽象方法。
@Database(entities = [Song::class, Album::class], version = 1)
abstract class MediaDatabase: RoomDatabase() {
abstract fun songDao(): SongDao
abstract fun albumDao(): AlbumDao
companion object {
@Volatile
var INSTANCE: MediaDatabase? = null
fun getDatabase(context: Context): MediaDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(context.applicationContext, MediaDatabase::class.java, "media.db")
.enableMultiInstanceInvalidation()
.build()
INSTANCE = instance
instance
}
}
}
}
定义 Dao
DAO 作为访问数据库的 API,提供CRUD方法
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import com.example.room.db.entities.Album
import com.example.room.db.entities.Song
@Dao
interface SongDao {
@Insert
fun insertSong(song: Song)
@Insert
fun insertAll(vararg songs: Song)
@Delete
fun delete(song: Song)
}
@Dao
interface AlbumDao {
@Insert
fun insertAlbum(album: Album)
}
Type Converters
对于自定义类型的成员数据,很多情况下也需要持久化到数据库,可以使用 @TypeConverter
class Converters {
@TypeConverter
fun tsToData(ts: Long): Date = Date(ts)
@TypeConverter
fun dataToTs(date: Date) = date.time
@TypeConverter
fun jsonToTags(json: String?): List<Tag> {
return if (json == null) emptyList() else Gson().fromJson(json, object : TypeToken<List<Tag>>() {}.type)
}
@TypeConverter
fun tagsToJson(tags: List<Tag>?): String? {
return if (tags == null) null else Gson().toJson(tags)
}
}
Converter 需要在声明 Database 时使用@TypeConverters指定:
@Database(entities = [Song::class, Album::class], version = 1)
@TypeConverters(Converters::class)
abstract class MediaDatabase: RoomDatabase() {}
到此,Room 的基本组件就齐活了。
DAO 操作数据库
Room Database 在使用中都是通过DAO对数据库读写。可以与LiveData、RxJava、Coroutine方便的集成。Room在编译时通过DAO中定义的方法来生成对应的具体实现,同时可以检查SQL的正确性,方便、友好。
@Insert
@Insert 注解插入操作,以单独的事务更新到数据库。
@Dao
interface SongDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertSong(song: Song)
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insertAll(vararg songs: Song)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertAll(songs: List<Song>)
}
onConflict:遭遇冲突时的解决策略,默认值为ABORT
- REPLACE: 新值替换旧值
- ABORT: 结束当前事务,回滚
- IGNORE: 返回成功插入的 row-ids,忽略其中有冲突的 Rows
@Update
@Update 注解更新操作,根据主键更新指定 rows
@Update(onConflict = OnConflictStrategy.REPLACE)
fun updateSong(song: Song)
@Update
fun updateAll(songs: List<Song>)
@Update
fun updateSongs(vararg songs: Song)
@Delete
@Delete 注解删除操作,根据主键删除指定 rows
@Delete
fun delete(song: Song)
@Delete
fun deleteAll(songs: List<Song>)
@Query
@Query 注解查询操作,参数为SQL,并且SQL和返回值在编译时会进行检查。
@Query("SELECT * FROM song")
fun queryAllSongs():List<Song>
Query 参数
SQL 语句中可以通过 ":" 引用WHERE子句中所需参数:
@Query("SELECT * FROM song WHERE duration < :target")
fun querySongsWitchDurationLessThanGiven(target: Long): List<Song>
查询非全部Column
播放列表仅需要显示歌曲名和歌手的情形:
data class PlaylistItem(val name: String, val artist: String)
// Dao:
@Query("SELECT name, artist FROM song")
fun queryAllPlaylistItem(): List<PlaylistItem>
Cursor
貌似没什么使用场景
@Query("SELECT * FROM song")
fun loadAllSongsCursor(): Cursor
多表查询
data class SongInfo(
val id: Long,
val name: String,
val artist: String,
@ColumnInfo(name = "album_name")
val albumName: String,
val duration: Long
)
// Query:
@Query(
"""
SELECT song.id, song.name, song.artist, album.album_name, song.duration
FROM song
JOIN album
ON song.albumId = album.id
"""
)
fun queryAllSongsWithAvailableAlbum(): List<SongInfo>
Entitiy / 表关系
SQLite 是关系行数据库,使用中需要讲数据拆分到具体的Entity,Entities之间允许多种关系,Room 也提供了这些关系的实现方式。
@Embedded 内嵌对象
@Embedded 注解可以将一个Entity作为一个属性内嵌到另一个Entity,访问这个内部Entity就像访问Column一样
@Entity(indices = [Index(value = ["id", "name"], unique = true)])
data class Song(
@PrimaryKey
val id: Long,
val name: String,
val artist: String,
val duration: Long,
val albumId: Long
)
@Entity
data class Album(
@PrimaryKey
val id: Long,
val name: String,
@ColumnInfo(name = "front_cover")
val img: String,
@Embedded(prefix = "song_")
val song: Song
)
由于
Song和Album中都包含name属性,可以通过prefix参数指定前缀
上面的例子中等价于 Album包含了Song的全部属性,并且数据表中引用的Song的属性字段全部以"song_"开头:
@Embedded 注解方式建立的表关系比较粗暴。其它更复杂一点的关系如何处理呢?
一对一
对应用户(User)与曲库(Library)的关系,一个用户只拥有一个曲库,一个曲库只属于一个用户:
@Entity
data class User(
@PrimaryKey
val uid: Long,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "last_name")
val lastName: String
)
@Entity
data class Library(
@PrimaryKey
val libid: Long,
val title: String,
@ColumnInfo(name = "owner_id")
val ownerId: Long
)
为了建立两个实体的一对一关系,需要创建一个新的数据类,其中包含一个父Entity和一个子Entity,使用 @Relation 注解到子实体的实例,并设置对应的 parentColumn 和 entityColumn:
data class UserWithLibrary(
@Embedded
val user: User,
@Relation(
parentColumn = "uid",
entityColumn = "owner_id"
)
val library: Library
)
接下来在Dao中加入Query:
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
这个Query实际运行两次查询,所以需要添加@Transcation, 确保整个查询的原子性
一对多
用户(User)与播放列表(Playlist)的关系,一个用户可以创建多个列表,一个列表的创建者只能是一个具体的用户:
@Entity
data class User(
@PrimaryKey
val uid: Long,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "last_name")
val lastName: String
)
@Entity
data class Playlist(
@ColumnInfo(name = "playlist_id")
@PrimaryKey val playlistId: Long,
@ColumnInfo(name = "creator_id")
val creatorId: Long,
@ColumnInfo(name = "playlist_name")
val playlistName: String
)
在 User 和 Playlist 之间建立一对多关系:创建一个新的数据类,其中每个实例都包含父实体的一个实例和子实体的实例列表,使用@Relation 添加到子实体,并设置parentColumn为父实体的主键,entityColumn 设置为引用父实体主键的子实体列名称:
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "uid",
entityColumn = "creator_id"
)
val playlists: List<Playlist>
)
Query:
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>
多对多
播放列表(Playlist)与歌曲(Song)存在多对多关系,每个播放列表可包含多个歌曲,每个歌曲也可处于多个播放列表中,由于多对多关系不能严格界定父实体与子实体,所以也就不存在子实体对父实体的引用,因此需要创建另外一个数据类来作为两个实体的桥梁。实现交叉引用,必须包含每个实体的主键列:
@Entity
data class Playlist(
@ColumnInfo(name = "playlist_id")
@PrimaryKey val playlistId: Long,
@ColumnInfo(name = "creator_id")
val creatorId: Long,
@ColumnInfo(name = "playlist_name")
val playlistName: String
)
@Entity
data class Song(
@PrimaryKey
@ColumnInfo(name = "song_id")
val songId: Long,
@ColumnInfo(name = "song_name")
val songName: String,
val artist: String
)
@Entity(primaryKeys = ["playlist_id", "song_id"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
在这种情况下,可以在不同情况下查询不同的实体:
-
查询播放列表并包含列表中所包含的歌曲:
data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation( parentColumn = "playlist_id", entityColumn = "song_id", associateBy = @Junction(PlaylistSongCrossRef::class) ) val songs: List<Song> ) -
查询歌曲所在的所有播放列表:
data class SongWithPlaylists( @Embedded val song: Song, @Relation( parentColumn = "song_id", entityColumn = "playlist_id", associateBy = @Junction(PlaylistSongCrossRef::class) ) val playlists: List<Playlist> )
上述每个类中的@Relation注解中使用 associateBy 来明确两个实体之间关系的桥接实体。
Query:
@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>
@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>
嵌套关系
基于以上关系的定义,@Relation 同样可以嵌套使用:
在用户(User)、播放列表(Playlist)、歌曲(Song)中就存在这样的嵌套关系:
- 用户可创建多个播放列表(一对多)
- 播放列表可以包含多个歌曲(多对多)
- 每个歌曲可以出现在多个播放列表中
如果要列出用户所有的播放列表详情,就需要将以上建立的关系嵌套到新的关系中:
data class UserWithPlaylistsAndSongs {
@Embedded val user: User
@Relation(
entity = Playlist::class,
parentColumn = "uid",
entityColumn = "creator_id"
)
val playlists: List<PlaylistWithSongs>
}
Query:
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
[!WARNING]
嵌套关系查询数据库需要多次操作,处理大量数据,可能会影响性能,尽量避免或减少嵌套关系