Android体系结构组件:Room Persistence库

本文详细介绍了Android的Room持久性库,它是SQLite数据库的抽象层,提供了编译时查询验证和易于使用的API。文章涵盖了Room的组件,如数据库、DAO和实体,以及如何设置环境、创建数据库、数据类型转换等。通过Room,开发者可以更高效地进行数据库操作,同时结合LiveData实现数据的实时更新。
摘要由CSDN通过智能技术生成

Android Architecture Components系列的最后一篇文章中,我们将探索Room持久性库, Room持久性库是一种出色的新资源,它使在Android中使用数据库更加容易。 它在SQLite,编译时检查的SQL查询以及异步和可观察的查询上提供了一个抽象层。 Room将Android上的数据库操作提升到另一个层次。

由于这是该系列的第四部分,所以我假设您熟悉Architecture软件包的概念和组件,例如LiveDataLiveModel 。 但是,如果您没有阅读最后三篇文章中的任何一篇,您仍然可以关注。 不过,如果您对这些组件不了解太多,请花一些时间阅读该系列-您可能会喜欢。

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 NULLDEFAULTCHECK (至少直到现在,在1.0.0版中),但是您可以在Entity上创建自己的逻辑以获得相似的结果。 为了避免Kotlin实体上出现空值,只需删除? 在变量类型的末尾,或者在Java中,添加@NonNull批注。

对象之间的关系

与大多数对象关系映射库不同,Room不允许实体直接引用另一个实体。 这意味着,如果您有一个名为NotePad的实体和一个名为Note的实体,则无法像在许多类似的库中那样在NotePad创建NoteCollection 。 最初,这种限制看起来很烦人,但这是将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常量定义了发生冲突时要遵循的策略。 选项几乎是不言自明的,其中ABORTFAILREPLACE是更重要的可能性。

要更新实体,请使用@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无法自动识别的更复杂的对象,例如DateEnum

为了让Room理解数据转换,有必要提供TypeConverters并在Room中注册这些转换器。 可以考虑特定上下文进行此注册-例如,如果您在Database注册TypeConverter ,则Database所有实体都将使用该转换器。 如果您在实体上注册,则只有该实体的属性可以使用它,依此类推。

要转换Date直接对象到Long房的节能行动中,然后转换为LongDate咨询数据库时,首先声明一个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 ,让我们提供DatabaseWeatherDAO

@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数据库的操作。 首先,我们添加一个MutableLiveDataweatherDB ,它负责查询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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值