In dit laatste artikel van de serie Android-architectuurcomponenten zullen we de bibliotheek Persisteren van ruimten verkennen, een uitstekende nieuwe bron die het een stuk eenvoudiger maakt om met databases in Android te werken. Het biedt een abstractielaag over SQLite, gecompileerde gecorrigeerde SQL-query's en ook asynchrone en waarneembare query's. Room tilt databasebewerkingen op Android naar een ander niveau.
Aangezien dit het vierde deel van de serie is, ga ik ervan uit dat u bekend bent met de concepten en componenten van het Architectuur-pakket, zoals LiveData en LiveModel. Als u echter geen van de laatste drie artikelen hebt gelezen, kunt u deze nog steeds volgen. Maar als je niet veel weet over die componenten, neem dan de tijd om de serie te lezen - je vindt het misschien leuk.
Zoals eerder vermeld, is Room geen nieuw databasesysteem. Het is een abstracte laag die de standaard SQLite-database omhult die door Android is geadopteerd. Room voegt echter zoveel functies toe aan SQLite dat het bijna onmogelijk te herkennen is. Room vereenvoudigt alle database-gerelateerde bewerkingen en maakt ze ook veel krachtiger omdat het de mogelijkheid biedt om observables en gecompileerde SQL-query's te retourneren.
Kamer bestaat uit drie hoofdcomponenten: de Database, de DAO (Data Access Objects) en de Entiteit. Elke component heeft zijn verantwoordelijkheid en ze moeten allemaal worden geïmplementeerd zodat het systeem kan werken. Gelukkig is zo'n implementatie vrij eenvoudig. Dankzij de voorziene annotaties en abstracte klassen, wordt de standaard om Room te implementeren tot een minimum beperkt.
@Entiteit
.@Dao
die de toegang tot objecten in de database en de tabellen bemiddelt. Er zijn vier specifieke annotaties voor de basis DAO-bewerkingen: @Insert
, @Bijwerken
, @Delete
, en @Query
.@Database
, welke zich uitstrekt RoomDatabase
. De klasse definieert de lijst met entiteiten en de bijbehorende DAO's.Om Room te gebruiken, voeg de volgende afhankelijkheden toe aan de app-module in Gradle:
compileer "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
Als u Kotlin gebruikt, moet u de Kapt
plugin en voeg een andere afhankelijkheid toe.
plug-in toepassen: 'kotlin-kapt' // ... dependencies // ... kapt "android.arch.persistence.room:compiler:1.0.0"
Een Entiteit vertegenwoordigt het object dat in de database wordt opgeslagen. Elk Entiteit
class maakt een nieuwe databasetabel, waarbij elk veld een kolom voorstelt. Annotaties worden gebruikt om entiteiten te configureren, en hun creatieproces is heel eenvoudig. Merk op hoe eenvoudig het is om een Entiteit
gebruik van Kotlin-dataklassen.
@ Entity-gegevensklasse Opmerking (@PrimaryKey (autoGenerate = true) var id: Long?, Var text: String ?, var date: Long?)
Zodra een klasse is geannoteerd met @Entiteit
, de roombibliotheek maakt automatisch een tabel met de klassevelden als kolommen. Als u een veld moet negeren, annoteer het dan gewoon met @Negeren
. elk Entiteit
moet ook een definiëren @Hoofdsleutel
.
Room gebruikt de klasse en de veldnamen om automatisch een tabel te maken; u kunt echter de gegenereerde tabel personaliseren. Gebruik de om een naam voor de tabel te definiëren tafel naam
optie op de @Entiteit
annotatie en om de naam van de kolom te bewerken, voeg een toe @ColumnInfo
annotatie met de naamoptie op het veld. Het is belangrijk om te onthouden dat de tabel- en kolomnamen hoofdlettergevoelig zijn.
@Entity (tableName = "tb_notes") dataklasse Note (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "_id") var id: Long ?, // ...)
Er zijn enkele bruikbare SQLite-beperkingen die Room ons toestaat om eenvoudig te implementeren op onze entiteiten. Om de zoekopdrachten te versnellen, kunt u SQLite maken index op de velden die relevanter zijn voor dergelijke zoekopdrachten. Indices zullen zoekopdrachten veel sneller uitvoeren; Ze zullen echter ook vragen langzamer invoegen, verwijderen en bijwerken, dus u moet ze zorgvuldig gebruiken. Bekijk de SQLite-documentatie om ze beter te begrijpen.
Er zijn twee verschillende manieren om indices in Room te maken. U kunt gewoon de ColumnInfo
eigendom, inhoudsopgave
, naar waar
, laat Room de indices voor je instellen.
@ColumnInfo (naam = "date", index = true) var date: Long
Of, als u meer controle nodig heeft, gebruikt u de index
eigendom van de @Entiteit
annotatie, met een lijst met de namen van de velden waaruit de index moet bestaan in de waarde
eigendom. Merk op dat de volgorde van items in waarde
is belangrijk omdat het het sorteren van de indextabel definieert.
@Entity (tableName = "tb_notes", indices = arrayOf (Index (waarde = * arrayOf ("date", "title"), name = "idx_date_title")))
Een andere handige SQLite-beperking is uniek
, die het gemarkeerde veld verbiedt dubbele waarden te hebben. Helaas biedt Room in versie 1.0.0 deze eigenschap niet op de juiste manier, rechtstreeks op het entiteitsveld. Maar u kunt een index maken en deze uniek maken en een vergelijkbaar resultaat bereiken.
@Entity (tableName = "tb_users", indices = arrayOf (Index (value = "gebruikersnaam", name = "idx_username", unique = true)))
Andere beperkingen zoals NIET NUL
, STANDAARD
, en CONTROLEREN
zijn niet aanwezig in Room (ten minste tot nu toe, in versie 1.0.0), maar u kunt uw eigen logica op de entiteit maken om vergelijkbare resultaten te bereiken. Om null-waarden op Kotlin-entiteiten te vermijden, verwijdert u gewoon de ?
aan het einde van het variabele type of, in Java, voeg de @NonNull
aantekening.
In tegenstelling tot de meeste object-relationele mapping-bibliotheken staat Room niet toe dat een entiteit rechtstreeks naar een andere verwijst. Dit betekent dat als u een entiteit heeft gebeld NotePad
en één geroepen Notitie
, je kunt geen Verzameling
van Notitie
s in de NotePad
zoals je zou doen met veel vergelijkbare bibliotheken. In eerste instantie leek deze beperking vervelend, maar het was een ontwerpbeslissing om de Room-bibliotheek aan de architectuurbeperkingen van Android aan te passen. Om deze beslissing beter te begrijpen, kun je de uitleg van Android voor hun aanpak bekijken.
Hoewel de objectrelatie van Room beperkt is, bestaat deze nog steeds. Met behulp van externe sleutels is het mogelijk om te verwijzen naar bovenliggende en onderliggende objecten en hun wijzigingen cascade toe te passen. Merk op dat het ook wordt aanbevolen om een index op het onderliggende object te maken om volledige tabelscans te voorkomen wanneer het bovenliggende object wordt gewijzigd.
@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)) ) dataklasse Opmerking (@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)
Het is mogelijk om objecten in entiteiten in te bedden met behulp van de @Embedded
annotatie. Nadat een object is ingesloten, worden alle velden als kolommen in de tabel van de entiteit toegevoegd, waarbij de veldnamen van het ingesloten object als kolomnamen worden gebruikt. Beschouw de volgende code.
dataklasse Locatie (var lat: Float, var lon: Float) @Entity (tableName = "tb_notes") dataklasse Opmerking (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "note_id") var id: Long, @Embedded (prefix = "note_location_") var location: Location?)
In de bovenstaande code, de Plaats
klasse is ingebed in de Notitie
entiteit. De tabel van de entiteit heeft twee extra kolommen, die overeenkomen met de velden van het ingesloten object. Omdat we de prefix-eigenschap gebruiken op de @Embedded
annotatie, de namen van de kolommen zijn 'note_location_lat
'en'note_location_lon
', en het is mogelijk om in query's naar die kolommen te verwijzen.
Voor toegang tot de Databases is een DAO-object noodzakelijk. De DAO kan worden gedefinieerd als een interface of een abstracte klasse. Om het te implementeren, annoteert u de klasse of interface met @Dao
en je bent goed om toegang tot gegevens te krijgen. Hoewel het mogelijk is om toegang te krijgen tot meer dan één tabel van een DAO, wordt aangeraden om, in de naam van een goede architectuur, het principe van scheiding van zorgen te behouden en een DAO te maken die verantwoordelijk is voor toegang tot elke entiteit.
@Dao interface NoteDAO
Room biedt een reeks handige annotaties voor de CRUD-bewerkingen in de DAO: @Insert
, @Bijwerken
, @Delete
, en @Query
. De @Insert
operatie kan een enkele entiteit ontvangen, rangschikking
, of a Lijst
van entiteiten als parameters. Voor afzonderlijke entiteiten wordt mogelijk a lang
, voor de rij van de invoeging. Voor meerdere entiteiten als parameters kan het een a opleveren lang[]
of a Lijst
in plaats daarvan.
@Insert (onConflict = OnConflictStrategy.REPLACE) fun insertNote (let op: Opmerking): Long @Insert (onConflict = OnConflictStrategy.ABORT) fun insertNotes (notes: List): Lijst
Zoals je kunt zien, is er nog een andere eigenschap om over te praten: onConflict
. Dit definieert de strategie die moet worden gevolgd in geval van conflicten OnConflictStrategy
constanten. De opties zijn vrijwel vanzelfsprekend, met ABORT
, FAIL
, en VERVANGEN
de meer significante mogelijkheden zijn.
Als u entiteiten wilt bijwerken, gebruikt u de @Bijwerken
annotatie. Het volgt hetzelfde principe als @Insert
, het ontvangen van enkele entiteiten of meerdere entiteiten als argumenten. Room gebruikt de ontvangende entiteit om de waarden bij te werken, met behulp van de entiteit Hoofdsleutel
als referentie. echter, de @Bijwerken
mag alleen een int
vertegenwoordigt het totaal van de bijgewerkte tabelrijen.
@Update () leuke updateNote (let op: Opmerking): Int
Nogmaals, volgens hetzelfde principe, de @Delete
annotatie kan enkele of meerdere entiteiten ontvangen en een int
met het totaal van tabelrijen bijgewerkt. Het maakt ook gebruik van de entiteit Hoofdsleutel
om het register in de tabel van de database te vinden en te verwijderen.
@ Verwijder leuk deleteNote (let op: Opmerking): Int
eindelijk, de @Query
annotatie maakt overleg in de database. De query's zijn op dezelfde manier opgebouwd als SQLite-query's, met als grootste verschil de mogelijkheid om argumenten rechtstreeks van de methoden te ontvangen. Maar het belangrijkste kenmerk is dat de query's tijdens het compileren worden geverifieerd, wat betekent dat de compiler een fout zal vinden zodra u het project bouwt.
Als u een query wilt maken, annoteert u een methode met @Query
en schrijf een SQLite-query als waarde. We zullen niet te veel aandacht besteden aan het schrijven van query's omdat ze de standaard SQLite gebruiken. Maar over het algemeen gebruikt u query's om gegevens uit de database op te halen met behulp van de SELECT
commando. Selecties kunnen enkele of verzamelwaarden retourneren.
@Query ("SELECT * FROM tb_notes") fun findAllNotes (): List
Het is heel eenvoudig om parameters door te geven aan query's. Room zal de naam van de parameter afleiden met behulp van de naam van het methode-argument. Gebruik om toegang te krijgen :
, gevolgd door de naam.
@Query ("SELECT * FROM tb_notes WHERE note_id =: id") fun findNoteById (id: Long): Note @Query ("SELECT * FROM tbgenoot WHERE note_date TUSSEN: early AND: late") fun findNoteByDate (early: Date, late : Datum): lijst
Room is ontworpen om gracieus mee te werken Actuele gegevens
. Voor een @Query
terugsturen Actuele gegevens
, pak gewoon het standaard rendement in met Actuele gegevens
>
en je bent klaar om te gaan.
@Query ("SELECT * FROM tb_notes WHERE note_id =: id") fun findNoteById (id: Long): LiveData
Daarna is het mogelijk om het queryresultaat te bekijken en asynchrone resultaten vrij eenvoudig te krijgen. Als u de kracht van LiveData niet kent, neem dan de tijd om onze tutorial over het onderdeel te lezen.
De database is gemaakt door een abstracte klasse, geannoteerd met @Database
en uitbreiding van de RoomDatabase
klasse. Ook moeten de entiteiten die door de database worden beheerd, worden doorgegeven in een array in de entiteiten
eigendom in de @Database
aantekening.
@Database (entities = arrayOf (NotePad :: class, Note :: class)) abstract class Database: RoomDatabase () abstract fun padDAO (): PadDAO abstract fun noteDAO (): NoteDAO
Zodra de database klasse is geïmplementeerd, is het tijd om te bouwen. Het is belangrijk om te benadrukken dat het database-exemplaar idealiter maar één keer per sessie moet worden gebouwd, en de beste manier om dit te bereiken zou zijn om een injectiesysteem voor afhankelijkheid te gebruiken, zoals Dagger. We zullen nu echter niet in DI duiken, omdat het buiten het bestek van deze tutorial valt.
fun providesAppDatabase (): Database ga terug Room.databaseBuilder (context, Database :: class.java, "database") .build ()
Normaal gesproken kunnen bewerkingen in een Room-database niet worden uitgevoerd vanuit de UI-thread, omdat ze worden geblokkeerd en waarschijnlijk problemen voor het systeem veroorzaken. Als u echter uitvoering wilt afdwingen in de UI-thread, voegt u toe allowMainThreadQueries
naar de build-opties. In feite zijn er veel interessante opties voor het bouwen van de database en ik raad u aan de RoomDatabase.Builder
documentatie om de mogelijkheden te begrijpen.
Een kolom Datatype wordt automatisch door Room gedefinieerd. Het systeem zal uit het type van het veld afleiden welk soort SQLite-datatype beter is. Houd er rekening mee dat de meeste POJO van Java uit de doos zullen worden geconverteerd; het is echter noodzakelijk om gegevensconverters te maken voor het verwerken van meer complexe objecten die niet automatisch door Room worden herkend, zoals Datum
en Enum
.
Voor Room om de dataconversies te begrijpen, is het noodzakelijk om te voorzien TypeConverters
en registreer die converters in Room. Het is mogelijk om deze registratie rekening te houden met specifieke context, bijvoorbeeld als u de TypeConverter
in de Database
, alle entiteiten van de database zullen de converter gebruiken. Als u zich bij een entiteit registreert, kunnen alleen de eigenschappen van die entiteit deze gebruiken, enzovoort.
Om een te converteren Datum
object rechtstreeks naar een Lang
tijdens de opslagbewerkingen van Room en converteer vervolgens een Lang
naar een Datum
wanneer u de database raadpleegt, moet u eerst a TypeConverter
.
class DataConverters @TypeConverter fun from Timestamp (mills: Long?): Date? return if (mills == null) null else Datum (mills) @TypeConverter fun fromDate (date: Date?): Long? = datum? .time
Registreer dan de TypeConverter
in de Database
, of in een meer specifieke context als je wilt.
@Database (entities = arrayOf (NotePad :: class, Note :: class), version = 1) @TypeConverters (DataConverters :: class) abstracte klasse Database: RoomDatabase ()
De applicatie die we hebben ontwikkeld tijdens deze serie gebruikt Gedeelde voorkeuren
om weergegevens te cachen. Nu we weten hoe we Room moeten gebruiken, gebruiken we het om een meer geavanceerde cache te maken waarmee we gegevens in de cache in de stad kunnen ophalen en ook rekening kunnen houden met de weergegevens tijdens het ophalen van gegevens..
Laten we eerst onze entiteit creëren. We zullen al onze gegevens opslaan met alleen de WeatherMain
klasse. We hoeven alleen enkele aantekeningen toe te voegen aan de klas en we zijn klaar.
@Entity (tableName = "weer") dataklasse WeatherMain (@ColumnInfo (naam = "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 // ...
We hebben ook een DAO nodig. De WeatherDAO
zal CRUD-operaties in onze entiteit beheren. Merk op dat alle zoekopdrachten terugkeren Actuele gegevens
.
@Dao interface WeatherDAO @Insert (onConflict = OnConflictStrategy.REPLACE) leuke insert (w: WeatherMain) @ Verwijder leuk (w: WeatherMain) @Query ("SELECT * FROM weather" + "ORDER BY id DESC LIMIT 1") fun findLast (): LiveData@Query ("SELECT * FROM weather" + "WHERE city LIKE: city" + "ORDER BY date DESC LIMIT 1") fun findByCity (city: String): LiveData @Query ("SELECT * FROM weather" + "WHERE date < :date " + "ORDER BY date ASC LIMIT 1" ) fun findByDate( date: Long ): List
Eindelijk is het tijd om de. Te maken Database
.
@Database (entities = arrayOf (WeatherMain :: class), version = 2) abstracte klasse Database: RoomDatabase () abstract funky weather weatherDAO (): WeatherDAO
Oké, nu hebben we onze Room-database geconfigureerd. Het enige dat overblijft om te doen, is om ermee te verbinden Dolk
en gebruik het. In de datamodule
, laten we de Database
en de WeatherDAO
.
@Module class DataModule (val context: Context) // ... @Provides @Singleton fun providesAppDatabase (): Database ga terug Room.databaseBuilder (context, Database :: class.java, "database") .allowMainThreadQueries () .fallbackToDestructiveMigration ( ) .build () @Provides @Singleton fun providesWeatherDAO (database: Database): WeatherDAO return database.weatherDAO ()
Zoals u zich zou moeten herinneren, hebben we een repository die verantwoordelijk is voor de verwerking van alle gegevensbewerkingen. Laten we deze klasse blijven gebruiken voor het ruimtegebruik van de app. Maar eerst moeten we het providesMainRepository
methode van de datamodule
, om de WeatherDAO
tijdens de klassenbouw.
@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 ) / ...
De meeste methoden die we zullen toevoegen aan de MainRepository
zijn vrij eenvoudig. Het is de moeite waard om er beter naar te kijken clearOldData ()
, though. Hiermee worden alle gegevens die ouder zijn dan een dag gewist, waarbij alleen relevante weergegevens worden bewaard die in de database zijn opgeslagen.
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> info ("getWeatherByCity: $ city") retourneer openWeatherService.getWeatherByCity (stad) fun saveOnDb (weatherMain: WeatherMain) info ("saveOnDb: \ n $ weatherMain") weatherDAO.insert (weatherMain) fun getRecentWeather (): Actuele gegevens info ("getRecentWeather") return weatherDAO.findLast () fun getRecentWeatherForLocation (location: String): LiveData info ("getWeatherByDateAndLocation") return weatherDAO.findByCity (location) fun clearOldData () info ("clearOldData") val c = Calendar.getInstance () c.add (Calendar.DATE, -1) // ontvang weergegevens van 2 dagen geleden val oldData = weatherDAO.findByDate (c.timeInMillis) oldData.forEach w -> info ("Gegevens verwijderen voor '$ w.name': $ w.dt") weatherDAO.remove (w ) // ...
De MainViewModel
is verantwoordelijk voor het raadplegen van onze repository. Laten we wat logica toevoegen om onze operaties aan de Room-database aan te pakken. Eerst voegen we een toe MutableLiveData
, de weatherDB
, die verantwoordelijk is voor het raadplegen van de MainRepository
. Vervolgens verwijderen we verwijzingen naar Gedeelde voorkeuren
, onze cache alleen afhankelijk maken van de Room-database.
class MainViewModel @Inject constructor (private val repository: MainRepository): ViewModel (), AnkoLogger // ... // Weer opgeslagen in database privé var weatherDB: LiveData= MutableLiveData () // ... // We verwijderen de consultatie naar SharedPreferences // de cache exclusief maken voor 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))
Om onze cache relevant te maken, zullen we oude gegevens wissen telkens wanneer er een nieuwe weersoverleg wordt gemaakt.
privé var weatherByLocationResponse: LiveData> = Transformations.switchMap (locatie, l -> info ("weatherByLocation: \ nlocation: $ l") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByLocation (l)) private var weatherByCityResponse: LiveData > = Transformations.switchMap (cityName, city -> info ("weatherByCityResponse: city: $ city") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByCity (city))
Ten slotte bewaren we de gegevens in de Room-database telkens wanneer nieuw weer wordt ontvangen.
// Ontvangt bijgewerkte weerreactie, // stuur het naar de gebruikersinterface en bewaar het ook privé-plezier updateWeather (w: WeatherResponse) info ("updateWeather") // krijgt weer van vandaag val weatherMain = WeatherMain.factory (w) // opslaan op gedeelde voorkeuren repository.saveWeatherMainOnPrefs (weatherMain) // opslaan op db repository.saveOnDb (weatherMain) // update weerwaarde weather.postValue (ApiResponse (data = weatherMain))
Je kunt de volledige code in de GitHub repo voor dit bericht zien.
Eindelijk zijn we aan het einde van de reeks Android Architecture Components. Deze tools zullen uitstekende metgezellen zijn tijdens je Android-ontwikkelingsreis. Ik raad u aan door te gaan met het verkennen van de componenten. Probeer wat tijd te nemen om de documentatie te lezen.
En bekijk enkele van onze andere berichten over de ontwikkeling van Android-apps hier op Envato Tuts+!