În acest articol final al seriei Android Architecture Components, vom explora biblioteca Persistență cameră, o resursă excelentă care face mult mai ușor să lucreze cu bazele de date din Android. Acesta oferă un strat de abstractizare peste SQLite, interogări SQL verificate în timp util, precum și interogări asincrone și observabile. Camera efectuează operații baze de date pe Android la un alt nivel.
Deoarece aceasta este a patra parte a seriei, presupun că sunteți familiarizat cu conceptele și componentele pachetului de arhitectură, cum ar fi LiveData și LiveModel. Cu toate acestea, dacă nu ați citit nici unul din ultimele trei articole, veți putea totuși să le urmați. Totuși, dacă nu știți prea multe despre aceste componente, luați ceva timp pentru a citi seria - vă puteți bucura.
Așa cum am menționat, Camera nu este un nou sistem de baze de date. Este un strat abstract care împachetează baza de date standard SQLite adoptată de Android. Cu toate acestea, Room adaugă atât de multe caracteristici SQLite că este aproape imposibil de recunoscut. Camera simplifică toate operațiile bazate pe baze de date și, de asemenea, le face mult mai puternice, deoarece permite posibilitatea returnării observabilelor și interogărilor SQL verificate în timpul compilării.
Camera este compusă din trei componente principale: Bază de date, DAO (Obiecte de acces de date) și Entitate. Fiecare componentă are responsabilitatea sa și toate acestea trebuie implementate pentru ca sistemul să funcționeze. Din fericire, o astfel de implementare este destul de simplă. Datorită adnotărilor furnizate și a claselor abstracte, boilerplate-ul pentru implementarea camerei este menținut la minimum.
@Entitate
.@Dao
care mediază accesul la obiecte din baza de date și din tabelele sale. Există patru adnotări specifice pentru operațiile DAO de bază: @Introduce
, @Actualizați
, @Șterge
, și @Query
.@Bază de date
, care se extinde RoomDatabase
. Clasa definește lista Entităților și a DAO-urilor.Pentru a utiliza Room, adăugați următoarele dependențe la modulul app din Gradle:
compilați "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "șiroid.arch.persistence.room:compiler:1.0.0"
Dacă utilizați Kotlin, trebuie să aplicați kapt
plugin și adăugați o altă dependență.
aplicați pluginul: "klin-kapt" // ... dependencies // ... kapt "android.arch.persistence.room:compiler:1.0.0"
Un Entitate reprezintă obiectul care este salvat în baza de date. Fiecare Entitate
clasa creează un nou tabel de bază de date, fiecare câmp reprezentând o coloană. Adnotările sunt folosite pentru a configura entități, iar procesul lor de creare este foarte simplu. Observați cât de simplu este să configurați un Entitate
utilizând clasele de date Kotlin.
@Entity class data Notă (@PrimaryKey (autoGenerate = true) var id: Long ?, var text: String ?, var data: Long?)
Odată ce o clasă este adnotată cu @Entitate
, biblioteca de cameră va crea automat o tabelă utilizând câmpurile de clasă ca coloane. Dacă trebuie să ignorați un câmp, trebuie doar să îl adnotați @Ignora
. Fiecare Entitate
trebuie să definească și a @Cheia principala
.
Camera va folosi clasa și numele câmpului pentru a crea automat un tabel; cu toate acestea, puteți personaliza tabelul care este generat. Pentru a defini un nume pentru tabel, utilizați tableName
opțiune pe @Entitate
adnotare, și pentru a edita numele coloanelor, adăugați a @ColumnInfo
adnotare cu opțiunea de nume pe câmp. Este important să rețineți că numele tabelei și coloanelor sunt sensibile la minuscule.
@Normal (tableName = "tb_notes") clasa de date Notă (@PrimaryKey (autoGenerate = true) @ColumnInfo (nume = "_id") var id: Long ?, // ...)
Există câteva constrângeri SQLite utile pe care Camera ne permite să le implementăm cu ușurință pe entitățile noastre. Pentru a accelera interogările de căutare, puteți crea SQLite indicii la domeniile care sunt mai relevante pentru astfel de interogări. Indicii vor face interogările de căutare mult mai rapide; cu toate acestea, acestea vor face inserarea, ștergerea și actualizarea interogărilor mai lent, deci trebuie să le folosiți cu atenție. Uitați-vă la documentația SQLite pentru a le înțelege mai bine.
Există două moduri diferite de a crea indicii în cameră. Puteți seta pur și simplu ColumnInfo
proprietate, index
, la Adevărat
, lăsând Room să-ți stabilească indicii.
@ColumnInfo (nume = "data", index = true) var data: Long
Sau, dacă aveți nevoie de mai mult control, utilizați indicii
proprietate a @Entitate
adnotare, care enumeră numele câmpurilor care trebuie să compună indicele în valoare
proprietate. Observați că ordinea articolelor din valoare
este importantă deoarece definește sortarea tabelei index.
@Entity (tableName = "tb_notes", indices = arrayOf (index = value = arrayOf ("date", "title"
O altă constrângere SQLite utilă este unic
, care interzice campului marcat să aibă valori duplicate. Din păcate, în versiunea 1.0.0, Camera nu oferă această proprietate așa cum ar trebui, direct pe domeniul entității. Dar puteți crea un index și îl puteți face unic, obținând un rezultat similar.
@Entity (tableName = "tb_users", indices = arrayOf (index (value = "username", nume = "idx_username", unic =
Alte constrângeri cum ar fi NU NUL
, MOD IMPLICIT
, și VERIFICA
nu sunt prezente în Cameră (cel puțin până acum, în versiunea 1.0.0), dar puteți crea propria logică asupra entității pentru a obține rezultate similare. Pentru a evita valorile null în entitățile Kotlin, trebuie doar să eliminați ?
la sfârșitul tipului de variabilă sau, în Java, adăugați @NonNull
adnotare.
Spre deosebire de cele mai multe biblioteci de mapare obiect-relațională, Camera nu permite unei entități să facă referire directă la o altă entitate. Aceasta înseamnă că dacă aveți o entitate chemată Bloc Notes
și unul a sunat Notă
, nu puteți crea un Colectie
de Notă
s în interiorul Bloc Notes
așa cum ați face cu multe biblioteci similare. La început, această limitare poate părea enervantă, însă a fost o decizie de proiectare pentru a adapta biblioteca camerei la limitările arhitecturii Android. Pentru a înțelege mai bine această decizie, aruncăm o privire la explicația Android pentru abordarea lor.
Chiar dacă relația obiectului Room este limitată, ea încă mai există. Utilizând chei străine, este posibil să trimiteți obiecte părinte și copil și să le modificați în mod automat. Observați că este, de asemenea, recomandat să creați un index pe obiectul copil pentru a evita scanarea completă a mesei atunci când părintele este modificat.
@Intitul (tableName = "tb_notes", indices = arrayOf (index = value = arrayOf ("note_date", "note_title"), name = "idx_date_title" "idx_pad_note")), foreignKeys = arrayOf (ForeignKey (entită = NotePad :: clasă, parentColumns = arrayOf ("pad_id"), childColumns = arrayOf ("note_pad_id"), onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE)) ) clase de date Notă (@PrimaryKey (autoGenerate = true) @ColumnInfo (nume = "note_id") var id: Long, @ColumnInfo (name = "note_title") var titlu: String ?, @ColumnInfo var text: String, @ColumnInfo (nume = "notă_date") var dată: Long, @ColumnInfo (nume = "note_pad_id") var padId: Long)
Este posibil să încorporați obiecte în interiorul entităților folosind @Încorporat
adnotare. Odată ce un obiect este încorporat, toate câmpurile acestuia vor fi adăugate ca coloane în tabelul entității, utilizând numele câmpurilor obiectului încorporat ca nume de coloane. Luați în considerare următorul cod.
clasa de date Locul de amplasare (var lat: float, var lon: Float) @Entity (tableName = "tb_notes") clasa de date Notă (@PrimaryKey (autoGenerate = true) @ColumnInfo (nume = "note_id") var id: Long, @Embedded (prefix = "note_location_") var locație: Locație?)
În codul de mai sus, Locație
clasa este încorporată în Notă
entitate. Tabelul entității va avea două coloane suplimentare, corespunzătoare câmpurilor obiectului încorporat. Deoarece folosim proprietatea prefixului pe @Încorporat
adnotări, numele coloanelor vor fi "note_location_lat
' și 'note_location_lon
"și va fi posibilă trimiterea acestor coloane în interogări.
Pentru a accesa bazele de date ale camerei, este necesar un obiect DAO. DAO poate fi definită fie ca interfață, fie ca clasă abstractă. Pentru ao implementa, adnotați clasa sau interfața cu @Dao
și sunteți bine să accesați datele. Chiar dacă este posibil să se acceseze mai mult de un tabel dintr-un DAO, este recomandat, în numele unei arhitecturi bune, să se mențină principiul separării preocupărilor și să se creeze un DAO responsabil pentru accesarea fiecărei entități.
Interfața @Dao NoteDAO
Camera oferă o serie de adnotări convenabile pentru operațiile CRUD din DAO: @Introduce
, @Actualizați
, @Șterge
, și @Query
. @Introduce
operațiunea poate primi o singură entitate, o mulțime
, sau a Listă
de entități ca parametri. Pentru entitățile unice, se poate întoarce a lung
, reprezentând rândul inserției. Pentru mai multe entități ca parametri, se poate întoarce a lung[]
sau a Listă
in schimb.
@Insert (onConflict = OnConflictStrategy.REPLACE) distracție insertNote (notă: notă): Long @Insert (onConflict = OnConflictStrategy.ABORT) distracție insertNotes (note: Lista): Listă
După cum puteți vedea, există o altă proprietate pentru a vorbi despre: onConflict
. Aceasta definește strategia de urmat în cazul utilizării de conflicte OnConflictStrategy
constante. Opțiunile sunt destul de explicative, cu ABORT
, FAIL
, și A INLOCUI
fiind posibilitățile mai importante.
Pentru a actualiza entitățile, utilizați @Actualizați
adnotare. Se aplică același principiu ca și @Introduce
, primind entități unice sau mai multe entități ca argumente. Cameră va utiliza entitatea primitoare pentru a actualiza valorile sale, utilizând entitatea Cheia principala
ca referinta. Însă @Actualizați
poate returna doar un int
reprezentând totalul rândurilor de tabel actualizate.
@ Update () fun updateNote (notă: Notă): Int
Din nou, urmând același principiu, @Șterge
adnotarea poate primi entități unice sau multiple și să returneze o int
cu actualizarea totală a rândurilor de tabelă. De asemenea, utilizează entitatea entității Cheia principala
pentru a găsi și elimina registrul în tabelul bazei de date.
@Delete fun deleteNote (notă: notă): Int
În cele din urmă, @Query
adnotarea face consultări în baza de date. Interogările sunt construite în mod similar cu interogările SQLite, cea mai mare diferență fiind posibilitatea de a primi argumente direct din metode. Dar cea mai importantă caracteristică este că interogările sunt verificate la momentul compilării, ceea ce înseamnă că compilatorul va găsi o eroare imediat ce construiți proiectul.
Pentru a crea o interogare, adnotați o metodă cu @Query
și scrieți o interogare SQLite ca valoare. Nu vom acorda prea multă atenție modului de scriere a interogărilor, deoarece utilizează standardul SQLite. În general, veți utiliza interogări pentru a prelua date din baza de date folosind SELECTAȚI
comanda. Selecțiile pot întoarce valori unice sau de colectare.
@Query ("SELECT * FROM tb_notes") distracție findAllNotes (): Listă
Este foarte simplu să transmiteți parametrii la interogări. Cameră va deduce numele parametrului, folosind numele argumentului metodei. Pentru ao accesa, utilizați :
, urmată de numele.
@Query ("SELECT * FROM tb_notes WHERE note_id =: id") distracție findNoteById (id: Long): Notă @Query (devreme, : Data): Listă
Camera a fost proiectată să lucreze cu grație LiveData
. Pentru o @Query
pentru a returna a LiveData
, doar încheiați întoarcerea standard cu LiveData
>
și ești bine să pleci.
@Query ("SELECT * FROM tb_notes WHERE note_id =: id") distracție findNoteById (id: Long): LiveData
După aceasta, va fi posibil să observați rezultatul interogării și să obțineți rezultate asincrone destul de ușor. Dacă nu cunoașteți puterea LiveData, faceți ceva timp pentru a citi instrucțiunile despre componente.
Baza de date este creată de o clasă abstractă, adnotată cu @Bază de date
și extinderea RoomDatabase
clasă. De asemenea, entitățile care vor fi gestionate de baza de date trebuie să fie transmise într-o matrice din entități
proprietate în @Bază de date
adnotare.
@Database (entities = arrayOf (NotePad :: clasă, Notă :: clasă)) clasă abstractă Baza de date: RoomDatabase () abstract fun padDAO ()
După implementarea clasei bazei de date, este timpul să construim. Este important să subliniem că instanța bazei de date ar trebui construită în mod ideal o singură dată pe sesiune, iar cel mai bun mod de a realiza acest lucru ar fi utilizarea unui sistem de injecție de dependență, cum ar fi Dagger. Cu toate acestea, nu ne vom scufunda în DI acum, deoarece este în afara scopului acestui tutorial.
distracție oferăAppDatabase (): Baza de date return Room.databaseBuilder (context, Database :: class.java, "database") .build ()
În mod normal, operațiile din baza de date a camerei nu pot fi realizate din fila UI, deoarece acestea blochează și probabil vor crea probleme pentru sistem. Cu toate acestea, dacă doriți să forțați execuția pe fila UI, adăugați allowMainThreadQueries
la opțiunile de construire. De fapt, există multe opțiuni interesante pentru cum să construiți baza de date și vă sfătuiesc să citiți RoomDatabase.Builder
pentru a înțelege posibilitățile.
O coloană Datatype este definită automat de Room. Sistemul va deduce din tipul câmpului tipul de tip SQLite Datatype mai adecvat. Rețineți că cea mai mare parte a POJO-ului Java va fi transformată din cutie; cu toate acestea, este necesar să creați convertoare de date pentru a gestiona obiecte mai complexe, care nu sunt recunoscute de cameră în mod automat, cum ar fi Data
și enum
.
Pentru ca Room să înțeleagă conversiile de date, este necesar să le oferiți TypeConverters
și înregistrați acei convertori în cameră. Este posibil ca această înregistrare să țină cont de contextul specific - de exemplu, dacă înregistrați TypeConverter
în Bază de date
, toate entitățile din baza de date vor folosi convertorul. Dacă vă înregistrați într-o entitate, numai proprietățile acelei entități o pot folosi și așa mai departe.
Pentru a converti a Data
obiecteaza direct la a Lung
în timpul operațiunilor de salvare ale camerei și apoi convertiți a Lung
la a Data
la consultarea bazei de date, declarați mai întâi a TypeConverter
.
Class DataConverters @TypeConverter distracție de la Timestamp (mori: Long?): Data? return if (mills == null) null otherwise Data (mori) @TypeConverter fun fromDate (data: Data?): Long? = data? .time
Apoi, înregistrați TypeConverter
în Bază de date
, sau într-un context mai specific, dacă doriți.
@Database (entități = arrayOf (NotePad :: clasă, Notă :: clasă), version = 1) @TypeConverters (DataConverters :: class) clasă abstractă Baza de date: RoomDatabase
Aplicația pe care am dezvoltat-o în această serie a fost utilizată SharedPreferences
pentru a memora datele meteo. Acum, când știm cum să folosim Room, o vom folosi pentru a crea o memorie cache mai sofisticată care ne va permite să obținem datele memorate în cache de către oraș și să luăm în considerare, de asemenea, data de vreme în timpul preluării datelor.
Mai întâi, să creăm entitatea noastră. Vom salva toate datele folosind doar WeatherMain
clasă. Trebuie doar să adăugăm câteva adnotări la clasă și am terminat.
@ColumnInfo (nume = "oraș") var nume: String ?, @ColumnInfo (name = "temp_min") "var tempMin: Dublu ?, @ColumnInfo (nume =" temp_max ") var tempMax: Dublu ?, @ColumnInfo (nume =" principal " String ?, @ColumnInfo (nume = "icon") var icon: String?) @ColumnInfo (nume = "id") @PrimaryKey (autoGenerate = true) var id: Long = 0 // ...
Avem de asemenea nevoie de un DAO. WeatherDAO
va gestiona operațiunile CRUD în entitatea noastră. Observați că toate interogările se întorc LiveData
.
@Dao interfață WeatherDAO @Insert (onConflict = OnConflictStrategy.REPLACE) inserați distracție (w: WeatherMain) @Delete fun remove (w: WeatherMain) @Query ("SELECT * FROM weather" + "ORDER BY ID DESC LIMIT 1" findLast (): LiveData@Query ("SELECT * FROM weather" + "WHERE oraș asemănător: oraș" + "ORDER BY date DESC LIMIT 1") distracție findByCity (oraș: String) @Query ("SELECT * FROM weather" + "WHERE data < :date " + "ORDER BY date ASC LIMIT 1" ) fun findByDate( date: Long ): List
În cele din urmă, este timpul să creați Bază de date
.
@Database (entities = arrayOf (WeatherMain :: class), version = 2) clasă abstractă Baza de date: RoomDatabase () abstract weather weatherDAO (): WeatherDAO
Ok, acum avem configurația bazei noastre de date Room. Tot ce trebuie lăsat să faceți este să vă conectați Pumnal
și începeți să-l utilizați. În DataModule
, să oferim Bază de date
si WeatherDAO
.
@Module class DataModule (contextul val: Context) // ... @Provides @Singleton distracție furnizeazăAppDatabase (): Baza de date return Room.databaseBuilder (context, Database :: class.java, "database") .allowMainThreadQueries () .fallbackToDestructiveMigration ) .build () @Provide @Singleton distracție oferăWeatherDAO (bază de date: Bază de date): WeatherDAO return database.weatherDAO ()
După cum trebuie să vă amintiți, avem un depozit responsabil pentru manipularea tuturor operațiunilor de date. Să continuăm să folosim această clasă pentru solicitarea de date a camerei Apps. Dar, mai întâi, trebuie să editați providesMainRepository
metodă a DataModule
, pentru a include WeatherDAO
în timpul construcției de clasă.
@Module class DataModule (context val: Context) // ... @Provides @Singleton distracție furnizeazăMainRepository (openWeatherService: OpenWeatherService, prefsDAO: PrefsDAO, weatherDAO: WeatherDAO, locationLiveData: LocationLiveData): MainRepository returnWeatherService, prefsDAO, weatherDAO, locationLiveData ) / ...
Majoritatea metodelor pe care le vom adăuga la MainRepository
sunt destul de simple. Merită să te uiți mai atent la clearOldData ()
, deşi. Aceasta șterge toate datele mai vechi decât o zi, păstrând doar date meteorologice relevante salvate în baza de date.
class MainRepository @Inject constructor (privat val openWeatherService: OpenWeatherService, val prefsDAO privat: PrefsDAO, val propriu weatherDAO: WeatherDAO, localizare privată: LocationLiveData): AnkoLogger fun getWeatherByCity (oraș: String)> info ("getWeatherByCity: $ city") întoarce openWeatherService.getWeatherByCity (oraș) distracție saveOnDb (vremeaMain: WeatherMain) weatherOur.info (weatherMain) fun getRecentWeather LiveData info ("getRecentWeather") întoarcere weatherDAO.findLast () distracție getRecentWeatherForLocation (locație: String): LiveData info ("getWeatherByDateAndLocation") retur weatherDAO.findByCity (locație) distracție clearOldData () (info ("clearOldData") val c = Calendar.getInstance () c.add (Calendar.DATE, -1) de la 2 zile în urmă val oldData = weatherDAO.findByDate (c.timeInMillis) oldData.forEach w -> info ("Înlăturarea datelor pentru '$ w.name': $ w.dt") weatherDAO.remove ) // ...
MainViewModel
este responsabil pentru a face consultări la depozitul nostru. Să adăugăm o logică pentru a aborda operațiunile noastre în baza de date a camerei. Mai întâi, adăugăm a MutableLiveData
, weatherDB
, care este responsabilă pentru consultarea MainRepository
. Apoi, eliminăm referințele SharedPreferences
, făcând cache-ul nostru bazându-se numai pe baza de date a camerei.
class MainViewModel @Inject constructor (depozit privat val: MainRepository): ViewModel (), AnkoLogger // ... // Vremea salvată pe baza de date private var weatherDB: LiveData= MutableLiveData () // ... // Eliminăm consultarea la SharedPreferences // făcând cache-ul exclusiv pentru distracția privată a camerei getWeatherCached () info ("getWeatherCached") weatherDB = repository.getRecentWeather () weather.addSource (weatherDB, w -> info ("vremeDB: DB: \ n $ w") weather.postValue (ApiResponse (data = w)) weather.removeSursă (vremeDBSaved)
Pentru a face ca cache-ul nostru să fie relevant, vom șterge datele vechi de fiecare dată când se face o nouă consultare meteorologică.
private var weatherByLocationResponse: LiveData> = Transformations.switchMap (locație, l -> info ("WeatherByLocation: \ nlocație: $ l") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByLocation (l) private var weatherByCityResponse: > = Transformations.switchMap (orașName, oraș -> info ("weatherByCityResponse: oraș: $ city") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByCity
În cele din urmă, vom salva datele în baza de date Room de fiecare dată când se primește o nouă vreme.
// primeste raspuns actualizat la vreme, // trimite-l la UI si salveaza si distractia privata updateWeather (w: WeatherResponse) info ("updateWeather") // obtinerea vremii de astazi val weatherMain = WeatherMain.factory (w) // save pe preferințe partajate repository.saveWeatherMainOnPrefs (weatherMain) // salvați pe db repository.saveOnDb (weatherMain) // actualizați valoarea vremii weather.postValue (ApiResponse (data = weatherMain))
Puteți vedea codul complet în repo GitHub pentru această postare.
În sfârșit, suntem la încheierea seriei Android Architecture Components. Aceste instrumente vor fi companioni excelenți în călătoria dvs. de dezvoltare Android. Vă sfătuiesc să continuați să explorați componentele. Încercați să faceți ceva timp pentru a citi documentația.
Și verificați câteva dintre celelalte postări ale dezvoltării aplicațiilor Android aici pe Envato Tuts+!