3 - Eina ORM: Biblioteca ROOM
En el Tema 5 hem vist una eina ORM molt potent, Hibernate. Però no podem utilitzar-la en Android.
Hi ha unes quantes ORM que es poden utilitzar en Android, com per exemple GreenDAO o SugarORM. Tanmateix, anem a utilitzar una altra, la llibreria ROOM, que potser no és tan potent com les anteriors, però és la que s'està utilitzant més actualment per a mapejar Bases de Dades de SQLite.
Ho practicarem tot sobre un projecte nou anomenat Empleat_ROOM cosa que suposarà segurament que s'utilitze el paquet com.example.empleat_room. Per a fer-lo el més senzill possible, en ell únicament col·loquem un EditText per a mostrar en ell les dades dels empleats. Aquest seria el activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<EditText
android:text=""
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textMultiLine"
android:gravity="top" />
</RelativeLayout>
Dependències
Per a utilitzar ROOM hem d'incorporar en el build.gradle de l'aplicació la següent dependència:
plugins {
...
id 'kotlin-android-extensions' // Per a accedir als components del layout
id 'kotlin-kapt'
}
...
dependencies {
...
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
On hem posat punts suspensius per indicar que hem d'afegir aquestes línies, la primera al principi, i les altres dins del bloc de dependencies.
Components de ROOM
Els 3 component principals de ROOM són:
- Entity: representa una taula de la Base de Dades, i serà una classe a la qual es convertirà la taula. Utilitzarem l'anotació @Entity, i per a fer correspondre cada propietat amb un camp de la taula, utilitzarem les anotacions @PrimaryKey i @ColumnInfo. També podrem representar les claus externes amb @ForeignKey, però no les utilitzarem en aquestos apunts per a no saturar
- DAO: serà una interfície que contindrà els mètodes utilizats per a accedir a la Base de Dades.
- RoomDatabase: representarà la connexió a la Base de Dades
Anem a comentar cadascun d'aquestos components, i per a això ens basarem en l'exemple dels Empleats. Utilitzarem la Base de Dades Empleats.sqlite, que teniu en l'aula virtual, on de moment només tenim una taula EMPLEAT amb aquesta estructura:
EMPLEAT(num: Integer, nom: Text, depart Integer, edat: Integer, sou: Real)
Entity
Per a cada taula que vulguem mapejar, haurem de crear una classe amb l'anotació @Entity. Com que aquesta classe és per a contenir dades, millor declarar-la com classe de dades (data class). Si la taula tinguera en la Base de Dades un nom diferent al de la classe, ho hauríem d'especificar entre parèntesis amb tableName
- El camp que siga clau principal el marcarem amb l'anotació @PrimaryKey
- Aquelles propietats que tinguen un nom diferent al nom del camp corresponent, els marcarem amb l'anotació @ColumnInfo seguida del nom del camp de la taula entre parèntesis
- També es poden definir claus externes amb ForeignKey, encara que és un poc més complicat
En el nostre exemple, la classe Empleat.kt ens quedaria així:
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Empleat (
@PrimaryKey val num: Int?,
val nom: String?,
@ColumnInfo(name = "depart") val departament: Int?,
val edat: Int?,
val sou: Float?
)
Observeu com:
- Al costat de @Entity no ha calgut posar el nom de la taula, perquè la classe i la taula es diran igual
- De les 5 propietats definides, només ha calgut posar l'anotació @ColumnInfo en la tercera per al departament, perquè és l'única en què la propietat no es diu igual que el camp de la taula
- Hem posat ? darrere de totes les propietats fins i tot en la que en la taula és clau principal, ja que no estan definides com a NOT NULL en la taula (tampoc la clau principal, encara que evidentment no pot ser nula)
DAO
Com ja hem comentat més amunt, el DAO serà una interfície que contindrà tots els mètodes per a accedir a la Base de Dades.
- Començarà amb l'anotació @DAO, i serà una interfície (interface)
- Els mètodes d'actualització, que són inserció, esborrat i actualització, començaran per l'anotacio @insert, @delete i @update respectivament, i són molt senzills perquè només hem de definir la funció, com veurem després en l'exemple. Hem de fer constar que posarem aquestos mètodes únicament pera il·lustrar-los, ja que en el nostre exemple no els utilitzarem.
- Els mètodes de consulta comencen amb l'anotació @Query seguida per la sentència SQL. En la definició de la funció haurem de dir el tipus tornat, normalment un List de la classe corresponent a la taula on fem la consulta. La consulta pot ser complicada, i fins i tot utilitzar paràmetres. En aquest exemple inicial, només farem la consulta completa de tota la taula EMPLEAT
El DAO del nostre exemple, que anomenarem EmpleatsDAO podria quedar de la següent manera:
import androidx.room.*
@Dao
interface EmpleatsDAO {
@Insert
fun insert(e: Empleat)
@Delete
fun delete(e: Empleat)
@Update
fun update(e: Empleat)
@Query("SELECT * FROM EMPLEAT")
fun getEmpleats() : List<Empleat>
@Query("SELECT * FROM EMPLEAT WHERE num = :n")
fun getEmpleat(n: Int) : Empleat
}
On recordem que els mètodes d'inserció, esborrat i modificació estan únicament amb motiu il·lustratiu. També el mètode GetEmpleat(n: Int), que serviria per agafar un empleat en concret a partir del seu número. Com que no els utilitzarem, perfectament els podríem suprimir. L'únic mètode que utilitzarem serà getEmpleats()
RoomDatabase
Representarà la connexió a la Base de Dades i serà una classe abstracta que heretarà de RoomDatabase. En ella:
- Utilitzarem l'anotació @Database
- Com a paràmetre de l'anterior anotació especificarem totes les entitats que volem mapejar (en el nostre exemple només és la classe Empleat). Com pot ser més d'una, s'ha de definir dins d'un Array.
- En ella definirem una funció que tornarà el DAO. També serà una funció abstracta
Definim doncs la classe EmpleatsDatabase, per exemple en un fitxer del mateix nom:
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = arrayOf(Empleat::class), version = 1)
abstract class EmpleatsDatabase:RoomDatabase() {
abstract fun empleatsDao(): EmpleatsDAO
}
Utilització des del programa principal
La manera de construir la connexió a la RoomDatabase és d'aquesta manera:
val db = Room.databaseBuilder(
applicationContext,
EmpleatsDatabase::class.java, "Empleats.sqlite"
).build()
Posteriorment veurem més possibilitats d'aquesta construcció. Per fer-lo de forma correcta, hauríem de tenir en compte que aquesta connexió és "costosa", i per tant hauríem d'utilitzar la tècnica del Singleton per assegurar-nos que només en creem una instància, encara que la puguem utilitzar més d'una vegada. Per senzillesa no ho farem en aquest exemple.
El que sí que tindrem en compte ara és que no es poden executar els accessos a la Base de Dades des del thread principal. La millor manera de solucionar açò és per mig de les corutines. Per senzillesa també, en aquest exemple ho farem de manera similar al punt 2, és a dir muntant un thread on es fan els accessos. En ell senzillament iniciem la RoomDatabase, inserim una nova empleada, i llistem tots els empleats, col·locant-los en un String. Des del programa principal, llancem el thread fill, i esperem a que finalitze (join). En aquest moment posem en el EditText la informació.
A continuació teniu tot el MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.room.Room
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
var cont: String = ""
private var sqlThread: Thread = object : Thread() {
override fun run() {
val db = Room.databaseBuilder(
applicationContext,
EmpleatsDatabase::class.java, "Empleats.sqlite"
)
.createFromAsset("Empleats.sqlite")
.build()
val e1 = Empleat(5,"Elena",10,25, 2500.0.toFloat())
db.empleatsDao().insert(e1)
var emps = db.empleatsDao().getEmpleats()
for (e in emps)
cont+=e.nom + " (" + e.sou + ")\n"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sqlThread.start()
// i ara esperem a que finalitze el thread fill unint-lo (join)
sqlThread.join()
text.setText(cont)
}
}
on en la construcció del RoomDatabase hem afegit la línia immediatament abans de construir-lo (.build() )
.createFromAsset("Empleats.sqlite")
per a no haver de pujar la Base de Dades Emplats.sqlite a mà des del Device File Explorer. Açò el que farà és que si no existeix la Base de Dades perquè és la primera vegada que s'executa, el carregarà des de la zona de assets. Per tant abans d'executar-lo:
- Creeu la zona de assets. Ho podeu fer fàcilment des del menú File -> New -> Folder -> Assets Folder, i quan us pregunte digueu-li que en el main
- Aneu a la vista Project i arrastreu el fitxer Empleats.sqlite a Empleat_ROOM -> app -> src -> main -> assets
Quan l'executem per primera vegada crearà el directori databases i dins d'ell copiarà Empleats.sqlite automàticament.
Llicenciat sota la Llicència Creative Commons Reconeixement NoComercial CompartirIgual 2.5