在Android Architecture Components系列的最后一篇文章中,我们将探索Room持久性库, Room持久性库是一种出色的新资源,它使在Android中使用数据库更加容易。 它在SQLite,编译时检查的SQL查询以及异步和可观察的查询上提供了一个抽象层。 Room将Android上的数据库操作提升到另一个层次。
由于这是该系列的第四部分,所以我假设您熟悉Architecture软件包的概念和组件,例如LiveData和LiveModel 。 但是,如果您没有阅读最后三篇文章中的任何一篇,您仍然可以关注。 不过,如果您对这些组件不了解太多,请花一些时间阅读该系列-您可能会喜欢。
1.房间部分
如上所述,Room不是新的数据库系统。 它是一个抽象层,包装了Android采用的标准SQLite数据库。 但是,Room向SQLite添加了太多功能,几乎无法识别。 Room简化了所有与数据库相关的操作,并使它们更加强大,因为它允许返回可观察值和编译时检查的SQL查询。
Room由三个主要组件组成: 数据库 , DAO (数据访问对象)和实体 。 每个组件都有自己的职责,并且所有这些组件都需要实现,才能使系统正常工作。 幸运的是,这种实现非常简单。 由于提供了注释和抽象类,用于实现Room的样板已降至最低。
- 实体是要保存在数据库中的类。 将为每个用
@Entity
注释的类创建一个专用数据库表。 - DAO是用
@Dao
注释的接口,它介导对数据库及其表中对象的访问。 还有的基本DAO操作四个具体注释:@Insert
,@Update
,@Delete
和@Query
。 - 数据库组件是带有
@Database
注释的抽象类,@Database
扩展了RoomDatabase
。 该类定义实体及其DAO的列表。
2.搭建环境
要使用Room,请将以下依赖项添加到Gradle中的应用模块:
compile "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
如果您使用的是Kotlin,则需要应用kapt
插件并添加另一个依赖项。
apply plugin: 'kotlin-kapt'
// …
dependencies {
// …
kapt "android.arch.persistence.room:compiler:1.0.0"
}
3.实体,数据库表
实体表示正在数据库中保存的对象。 每个Entity
类创建一个新的数据库表,每个字段代表一列。 注释用于配置实体,它们的创建过程确实很简单。 请注意,使用Kotlin数据类设置Entity
非常简单。
@Entity
data class Note(
@PrimaryKey( autoGenerate = true )
var id: Long?,
var text: String?,
var date: Long?
)
使用@Entity
注释类后,Room库将使用类字段作为列自动创建表。 如果您需要忽略字段,只需使用@Ignore
对其进行@Ignore
。 每个Entity
还必须定义一个@PrimaryKey
。
表和列
Room将使用该类及其字段名称自动创建表格; 但是,您可以个性化生成的表。 要定义表的名称,请使用@Entity
批注中的tableName
选项,并编辑列名,并在字段上添加带有name选项的@ColumnInfo
批注。 重要的是要记住表和列的名称区分大小写。
@Entity( tableName = “tb_notes” )
data class Note(
@PrimaryKey( autoGenerate = true )
@ColumnInfo( name = “_id” )
var id: Long?,
//...
)
指标和唯一性约束
Room允许我们轻松地在实体上实现一些有用的SQLite约束。 为了加快搜索查询的速度,您可以在与此类查询更相关的字段上创建SQLite 索引 。 索引将使搜索查询更快。 但是,它们也会使插入,删除和更新查询的速度变慢,因此您必须谨慎使用它们。 查看SQLite文档以更好地理解它们。
在Room中有两种创建索引的方法。 您可以简单地将ColumnInfo
属性index
设置为true
,让Room为您设置索引。
@ColumnInfo(name = "date", index = true)
var date: Long
或者,如果你需要更多的控制,使用该indices
的财产@Entity
注解,列出了必须在组成该指数的字段的名称value
属性。 请注意, value
的项顺序很重要,因为它定义了索引表的排序。
@Entity(
tableName = "tb_notes",
indices = arrayOf(
Index(
value = *arrayOf("date","title"),
name = "idx_date_title"
)
)
)
另一个有用的SQLite约束是unique
,它禁止标记字段具有重复值。 不幸的是,在1.0.0版中,Room并没有直接在实体字段上提供此属性。 但是您可以创建索引并使它唯一,从而获得相似的结果。
@Entity(
tableName = "tb_users",
indices = arrayOf(
Index(
value = “username”,
name = "idx_username",
unique = true
)
)
)
Room中不存在其他约束,例如NOT NULL
, DEFAULT
和CHECK
(至少直到现在,在1.0.0版中),但是您可以在Entity上创建自己的逻辑以获得相似的结果。 为了避免Kotlin实体上出现空值,只需删除?
在变量类型的末尾,或者在Java中,添加@NonNull
批注。
对象之间的关系
与大多数对象关系映射库不同,Room不允许实体直接引用另一个实体。 这意味着,如果您有一个名为NotePad
的实体和一个名为Note
的实体,则无法像在许多类似的库中那样在NotePad
创建Note
的Collection
。 最初,这种限制看起来很烦人,但这是将Room库调整为Android架构限制的一项设计决定。 为了更好地理解此决定,请查看Android对这种方法的解释 。
即使Room的对象关系受到限制,它仍然存在。 使用外键,可以引用父对象和子对象并对其进行级联。 注意,还建议在子对象上创建索引,以避免在修改父对象时进行全表扫描。
@Entity(
tableName = "tb_notes",
indices = arrayOf(
Index(
value = *arrayOf("note_date","note_title"),
name = "idx_date_title"
),
Index(
value = *arrayOf("note_pad_id"),
name = "idx_pad_note"
)
),
foreignKeys = arrayOf(
ForeignKey(
entity = NotePad::class,
parentColumns = arrayOf("pad_id"),
childColumns = arrayOf("note_pad_id"),
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
)
)
data class Note(
@PrimaryKey( autoGenerate = true )
@ColumnInfo( name = "note_id" )
var id: Long,
@ColumnInfo( name = "note_title" )
var title: String?,
@ColumnInfo( name = "note_text" )
var text: String,
@ColumnInfo( name = "note_date" )
var date: Long,
@ColumnInfo( name = "note_pad_id")
var padId: Long
)
嵌入物件
可以使用@Embedded
注解将对象嵌入实体中。 嵌入对象后,所有对象的字段都将作为列添加到实体表中,使用嵌入对象的字段名称作为列名称。 考虑以下代码。
data class Location(
var lat: Float,
var lon: Float
)
@Entity(tableName = "tb_notes")
data class Note(
@PrimaryKey( autoGenerate = true )
@ColumnInfo( name = "note_id" )
var id: Long,
@Embedded( prefix = "note_location_" )
var location: Location?
)
在上面的代码中, Location
类嵌入在Note
实体中。 实体的表将有两个额外的列,分别对应于嵌入对象的字段。 由于我们在@Embedded
批注上使用prefix属性,因此列的名称将为' note_location_lat
'和' note_location_lon
',并且可以在查询中引用这些列。
4.数据访问对象
要访问会议室的数据库,必须有一个DAO对象。 DAO可以定义为接口或抽象类。 要实现它,请使用@Dao
注释类或接口,您可以很好地访问数据。 即使可以从DAO访问多个表,还是建议以良好的体系结构名义维护“关注分离”原则并创建一个负责访问每个实体的DAO。
@Dao
interface NoteDAO{}
插入,更新和删除
客房提供了一系列在DAO的CRUD操作方便的注解: @Insert
, @Update
, @Delete
和@Query
。 @Insert
操作可以接收单个实体, array
或实体List
作为参数。 对于单个实体,它可能返回long
,表示插入的行。 对于多个实体作为参数,它可能返回long[]
或List<Long>
。
@Insert( onConflict = OnConflictStrategy.REPLACE )
fun insertNote(note: Note): Long
@Insert( onConflict = OnConflictStrategy.ABORT )
fun insertNotes(notes: List<Note>): List<Long>
如您所见,还有另一个要讨论的属性: onConflict
。 这使用OnConflictStrategy
常量定义了发生冲突时要遵循的策略。 选项几乎是不言自明的,其中ABORT
, FAIL
和REPLACE
是更重要的可能性。
要更新实体,请使用@Update
批注。 它遵循与@Insert
相同的原理,接收单个实体或多个实体作为参数。 Room将使用实体PrimaryKey
作为参考,使用接收实体来更新其值。 但是, @Update
可能仅返回一个int
,该int
表示已更新的表行的总数。
@Update()
fun updateNote(note: Note): Int
同样,遵循相同的原则, @Delete
批注可以接收单个或多个实体,并返回一个int
,其中表行的总数已更新。 它还使用实体的PrimaryKey
查找和删除数据库表中的寄存器。
@Delete
fun deleteNote(note: Note): Int
进行查询
最后, @Query
批注在数据库中进行咨询。 查询的构造与SQLite查询类似,最大的不同是可以直接从方法接收参数。 但是最重要的特征是查询在编译时进行了验证,这意味着编译器将在构建项目后立即发现错误。
要创建查询,请使用@Query
注释方法,然后将SQLite查询写为值。 由于查询使用标准SQLite,因此我们不会对其进行过多关注。 但通常,您将使用查询使用SELECT
命令从数据库检索数据。 选择可能返回单个值或集合值。
@Query("SELECT * FROM tb_notes")
fun findAllNotes(): List<Note>
将参数传递给查询真的很简单。 Room将使用方法参数的名称来推断参数的名称。 要访问它,请使用:
,后跟名称。
@Query("SELECT * FROM tb_notes WHERE note_id = :id")
fun findNoteById(id: Long): Note
@Query(“SELECT * FROM tb_noted WHERE note_date BETWEEN :early AND :late”)
fun findNoteByDate(early: Date, late: Date): List<Note>
LiveData查询
Room旨在与LiveData
。 要使@Query
返回LiveData
,只需用LiveData
<?>
结束标准返回,就可以了。
@Query("SELECT * FROM tb_notes WHERE note_id = :id")
fun findNoteById(id: Long): LiveData<Note>
5.创建数据库
该数据库由一个抽象类创建,该类带有@Database
批注并扩展了RoomDatabase
类。 此外,将由数据库管理的实体必须在阵列中通过entities
的财产@Database
注解。
@Database(
entities = arrayOf(
NotePad::class,
Note::class
)
)
abstract class Database : RoomDatabase() {
abstract fun padDAO(): PadDAO
abstract fun noteDAO(): NoteDAO
}
数据库类一旦实现,就可以构建了。 需要强调的是,理想情况下,数据库实例在每个会话中仅应构建一次,而实现此目标的最佳方法是使用依赖注入系统,例如Dagger 。 但是,由于DI不在本教程的讨论范围之内,所以我们现在不再讨论DI。
fun providesAppDatabase() : Database {
return Room.databaseBuilder(
context, Database::class.java, "database")
.build()
}
通常,无法通过UI线程对Room数据库进行操作,因为它们正在阻塞并且可能会给系统造成问题。 但是,如果要强制在UI线程上执行,则将allowMainThreadQueries
添加到构建选项。 实际上,关于如何构建数据库有很多有趣的选项,我建议您阅读RoomDatabase.Builder
文档以了解可能性。
6.数据类型和数据转换
列数据类型由Room自动定义。 系统将从字段的类型中推断出哪种SQLite数据类型更合适。 请记住,大多数Java的POJO都将立即转换。 但是,必须创建数据转换器来处理Room无法自动识别的更复杂的对象,例如Date
和Enum
。
为了让Room理解数据转换,有必要提供TypeConverters
并在Room中注册这些转换器。 可以考虑特定上下文进行此注册-例如,如果您在Database
注册TypeConverter
,则Database
所有实体都将使用该转换器。 如果您在实体上注册,则只有该实体的属性可以使用它,依此类推。
要转换Date
直接对象到Long
房的节能行动中,然后转换为Long
的Date
咨询数据库时,首先声明一个TypeConverter
。
class DataConverters {
@TypeConverter
fun fromTimestamp(mills: Long?): Date? {
return if (mills == null)
null
else Date(mills)
}
@TypeConverter
fun fromDate(date: Date?): Long? =
date?.time
}
然后,在Database
注册TypeConverter
,或者根据需要在更具体的上下文中注册。
@Database(
entities = arrayOf(
NotePad::class,
Note::class
),
version = 1
)
@TypeConverters(DataConverters::class)
abstract class Database : RoomDatabase()
7.在应用程序中使用房间
我们在本系列中开发的应用程序使用SharedPreferences
来缓存天气数据。 现在我们知道如何使用Room,我们将使用它来创建更复杂的缓存,该缓存将允许我们按城市获取缓存的数据,并在数据检索期间考虑天气日期。
首先,让我们创建我们的实体。 我们将仅使用WeatherMain
类保存所有数据。 我们只需要在类中添加一些注释,就可以完成了。
@Entity( tableName = "weather" )
data class WeatherMain(
@ColumnInfo( name = "date" )
var dt: Long?,
@ColumnInfo( name = "city" )
var name: String?,
@ColumnInfo(name = "temp_min" )
var tempMin: Double?,
@ColumnInfo(name = "temp_max" )
var tempMax: Double?,
@ColumnInfo( name = "main" )
var main: String?,
@ColumnInfo( name = "description" )
var description: String?,
@ColumnInfo( name = "icon" )
var icon: String?
) {
@ColumnInfo(name = "id")
@PrimaryKey(autoGenerate = true)
var id: Long = 0
// ...
我们还需要一个DAO。 WeatherDAO
将管理我们实体中的CRUD操作。 请注意,所有查询都返回LiveData
。
@Dao
interface WeatherDAO {
@Insert( onConflict = OnConflictStrategy.REPLACE )
fun insert( w: WeatherMain )
@Delete
fun remove( w: WeatherMain )
@Query( "SELECT * FROM weather " +
"ORDER BY id DESC LIMIT 1" )
fun findLast(): LiveData<WeatherMain>
@Query("SELECT * FROM weather " +
"WHERE city LIKE :city " +
"ORDER BY date DESC LIMIT 1")
fun findByCity(city: String ): LiveData<WeatherMain>
@Query("SELECT * FROM weather " +
"WHERE date < :date " +
"ORDER BY date ASC LIMIT 1" )
fun findByDate( date: Long ): List<WeatherMain>
}
最后,是时候创建Database
。
@Database( entities = arrayOf(WeatherMain::class), version = 2 )
abstract class Database : RoomDatabase() {
abstract fun weatherDAO(): WeatherDAO
}
好的,我们现在已经配置了Room数据库。 剩下要做的就是将它与Dagger
并开始使用它。 在DataModule
,让我们提供Database
和WeatherDAO
。
@Module
class DataModule( val context: Context ) {
// ...
@Provides
@Singleton
fun providesAppDatabase() : Database {
return Room.databaseBuilder(
context, Database::class.java, "database")
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build()
}
@Provides
@Singleton
fun providesWeatherDAO(database: Database) : WeatherDAO {
return database.weatherDAO()
}
}
您应该记住,我们有一个存储库负责处理所有数据操作。 让我们继续将此类用于应用程序的Room数据请求。 但首先,我们需要编辑providesMainRepository
的方法DataModule
,以包括WeatherDAO
类的施工过程中。
@Module
class DataModule( val context: Context ) {
//...
@Provides
@Singleton
fun providesMainRepository(
openWeatherService: OpenWeatherService,
prefsDAO: PrefsDAO,
weatherDAO: WeatherDAO,
locationLiveData: LocationLiveData
) : MainRepository {
return MainRepository(
openWeatherService,
prefsDAO,
weatherDAO,
locationLiveData
)
}
/…
}
我们将添加到MainRepository
中的大多数方法都非常简单。 不过,值得更仔细地查看clearOldData()
。 这将清除所有早于一天的数据,仅保留数据库中保存的相关天气数据。
class MainRepository
@Inject
constructor(
private val openWeatherService: OpenWeatherService,
private val prefsDAO: PrefsDAO,
private val weatherDAO: WeatherDAO,
private val location: LocationLiveData
) : AnkoLogger
{
fun getWeatherByCity( city: String ) : LiveData<ApiResponse<WeatherResponse>>
{
info("getWeatherByCity: $city")
return openWeatherService.getWeatherByCity(city)
}
fun saveOnDb( weatherMain: WeatherMain ) {
info("saveOnDb:\n$weatherMain")
weatherDAO.insert( weatherMain )
}
fun getRecentWeather(): LiveData<WeatherMain> {
info("getRecentWeather")
return weatherDAO.findLast()
}
fun getRecentWeatherForLocation(location: String): LiveData<WeatherMain> {
info("getWeatherByDateAndLocation")
return weatherDAO.findByCity(location)
}
fun clearOldData(){
info("clearOldData")
val c = Calendar.getInstance()
c.add(Calendar.DATE, -1)
// get weather data from 2 days ago
val oldData = weatherDAO.findByDate(c.timeInMillis)
oldData.forEach{ w ->
info("Removing data for '${w.name}':${w.dt}")
weatherDAO.remove(w)
}
}
// ...
}
MainViewModel
负责对我们的存储库进行咨询。 让我们添加一些逻辑以解决我们对Room数据库的操作。 首先,我们添加一个MutableLiveData
, weatherDB
,它负责查询MainRepository
。 然后,我们删除对SharedPreferences
引用,使我们的缓存仅依赖于Room数据库。
class MainViewModel
@Inject
constructor(
private val repository: MainRepository
)
: ViewModel(), AnkoLogger {
// …
// Weather saved on database
private var weatherDB: LiveData<WeatherMain> = MutableLiveData()
// …
// We remove the consultation to SharedPreferences
// making the cache exclusive to Room
private fun getWeatherCached() {
info("getWeatherCached")
weatherDB = repository.getRecentWeather()
weather.addSource(
weatherDB,
{
w ->
info("weatherDB: DB: \n$w")
weather.postValue(ApiResponse(data = w))
weather.removeSource(weatherDBSaved)
}
)
}
为了使我们的缓存具有相关性,每次进行新的天气咨询时,我们都会清除旧数据。
private var weatherByLocationResponse:
LiveData<ApiResponse<WeatherResponse>> = Transformations.switchMap(
location,
{
l ->
info("weatherByLocation: \nlocation: $l")
doAsync { repository.clearOldData() }
return@switchMap repository.getWeatherByLocation(l)
}
)
private var weatherByCityResponse:
LiveData<ApiResponse<WeatherResponse>> = Transformations.switchMap(
cityName,
{
city ->
info("weatherByCityResponse: city: $city")
doAsync { repository.clearOldData() }
return@switchMap repository.getWeatherByCity(city)
}
)
最后,每次收到新天气时,我们会将数据保存到Room数据库中。
// Receives updated weather response,
// send it to UI and also save it
private fun updateWeather(w: WeatherResponse){
info("updateWeather")
// getting weather from today
val weatherMain = WeatherMain.factory(w)
// save on shared preferences
repository.saveWeatherMainOnPrefs(weatherMain)
// save on db
repository.saveOnDb(weatherMain)
// update weather value
weather.postValue(ApiResponse(data = weatherMain))
}
您可以在GitHub存储库中查看此文章的完整代码。
结论
最后,我们就结束了Android Architecture Components系列。 这些工具将是您Android开发过程中的绝佳伴侣。 我建议您继续探索这些组件。 尝试花一些时间阅读文档 。
翻译自: https://code.tutsplus.com/tutorials/android-architecture-components-room--cms-29946