Explorem la resta de la configuració per a entitats i DAO
Introducció
Els únics requisits absoluts per a una classe d’entitat de Room és que estigui anotada amb l’anotació @Entity i tingui un camp identificat com a clau primària, típicament mitjançant una anotació @PrimaryKey. Qualsevol cosa més enllà d’això és opcional.
No obstant això, hi ha força coses que van “més enllà d’això”. Algunes — encara que probablement no totes — d’aquestes característiques són d’interès en aplicacions més grans.
Crea un projecte room-entity.
Claus primàries
Si tens un sol camp que és la clau primària per a la teva entitat, utilitzar l’anotació @PrimaryKey és simple i t’ajuda a identificar clarament aquesta clau primària en un punt posterior.
No obstant això, tens algunes altres opcions.
Claus primàries autogenerades
A SQLite, si tens una columna INTEGER identificada com a PRIMARY KEY, pots opcionalment fer que SQLite assigni valors únics per a aquesta columna, mitjançant la paraula clau AUTOINCREMENT.
A Room, si tens una propietat Long que és la teva @PrimaryKey, pots aplicar opcionalment AUTOINCREMENT a la columna corresponent afegint autoGenerate=true a l’anotació:
@Entity(tableName = "Party")data class Message( @PrimaryKey(autoGenerate = true) val id: Long = 0, val text: String,) { @Dao interface SQL { @Query("SELECT * FROM Party") suspend fun select(): List<Message>
@Query("SELECT * FROM Party WHERE id = :id") suspend fun selectWhereId(id: Long): Message?
@Insert suspend fun insert(autoGenerate: Message): Long }}
@androidx.room.Database(entities = [Message::class], version = 1)abstract class Database : RoomDatabase() { abstract fun message(): Message.SQL}Per defecte, autoGenerate és false. Establir aquesta propietat a true et dona AUTOINCREMENT a la instrucció CREATE TABLE generada:
CREATE TABLE IF NOT EXISTS Message (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, text TEXT NOT NULL)Els mètodes Insert tracten 0 (o null) com a no-establert mentre s’insereix l’element.
No coneixes la teva clau primària fins que insereixis l’entitat a una base de dades. Les teves funcions anotades amb @Insert poden retornar un resultat Long, i aquest serà la clau primària per a aquella entitat inserida.
val db = Room.inMemoryDatabaseBuilder<Database>().setDriver(BundledSQLiteDriver()).build()
db.message().select() shouldBe emptyList()
val id = db.message().insert(Message(text = "Hello World"))id shouldBe 1L
db.message().selectWhereId(1L)!!.let { it.id.shouldBe(1L); it.text shouldBe "Hello World"}UUID com a clau primària
Encara que una UUID ocupa molt més espai que un simple Long, poden ser generats de manera única fora de la base de dades.
Els Identificadors Únics Universals són números de 128 bits que sovint s’utilitzen com a identificadors de base de dades o de sessió. L’avantatge és que poden ser generats independentment en qualsevol lloc, amb una probabilitat extremadament baixa de generar el mateix ID dues vegades.
Això significa que pots crear aquests IDs i assignar-los a elements al costat del client en molts dispositius diferents, sense haver de sincronitzar constantment amb un servidor que et digui quin hauria de ser el pròxim ID.
Modifica la secció settings del fitxer module.yaml:
settings: kotlin: ksp: processors: - androidx.room:room-compiler:2.8.3 optIns: [ kotlin.uuid.ExperimentalUuidApi ]A continuació, crea una entitat Party amb un camp id de tipus UUID, anotat amb @PrimaryKey:
@Entity(tableName = "Party")data class Party( @PrimaryKey val id: String = Uuid.random().toString(), val name: String, val surname: String)Més informació a:
- UUID in Kotlin Multiplatform
- Identificadors Únics Universals com a Clau Primària en una Base de Dades Room
Claus primàries compostes
En alguns casos, pots tenir una clau primària composta, formada per dues o més columnes a la base de dades. Això és especialment cert si estàs intentant dissenyar les teves entitats al voltant d’una estructura de base de dades existent, una que utilitzava una clau primària composta per a una de les seves taules (per qualsevol raó).
Si, lògicament, aquestes són totes part d’un sol objecte, podries combinar-les en una sola propietat, com veurem a Room - Custom Type. No obstant això, pot ser que hagin de ser propietats individuals a la teva entitat, però que passin a combinar-se per crear la clau primària. En aquest cas, pots ometre l’anotació @PrimaryKey i utilitzar la propietat primaryKeys de l’anotació @Entity.
Un escenari per a això és el versionat de dades, on estem seguint canvis a les dades al llarg del temps, de la mateixa manera que un sistema de control de versions segueix els canvis al codi font i altres fitxers al llarg del temps. Hi ha diverses maneres d’implementar el versionat de dades.
Un enfocament té totes les versions de la mateixa entitat a la mateixa taula, amb un codi de versió adjunt a la clau primària “natural” per identificar una versió específica d’aquell contingut:
@Entity(tableName = "Document", primaryKeys = ["id", "version"])data class Document( val id: String = Uuid.random().toString(), val version: Int = 1, val title: String, val text: String,) { @Dao interface SQL { @Query("SELECT * FROM Document") suspend fun select(): List<Document>
@Query("SELECT * FROM Document WHERE id = :id AND version = :version") suspend fun selectWherePrimaryKey(id: String, version: Int): Document?
@Insert suspend fun insert(document: Document): Long }}Room utilitzarà llavors la paraula clau PRIMARY KEY a la instrucció CREATE TABLE per configurar la clau primària composta:
CREATE TABLE IF NOT EXISTS Document (id TEXT NOT NULL, version INTEGER NOT NULL, title TEXT NOT NULL, text TEXT NOT NULL, PRIMARY KEYKEY(id, version))Al nostre cas, establim version per tenir un valor per defecte de 1, així que podem crear un Document amb només un identificador de tipus string, almenys per a la seva versió inicial:
@Testfun test() = runTest {
val db = Room.inMemoryDatabaseBuilder<Database>().setDriver(BundledSQLiteDriver()).build()
val entity = Document(title = "Hello World", text = "A tiny step to begin something big.") db.document().insert(entity)
db.document().selectWherePrimaryKey(entity.id, 1)!!.let { it.id shouldBe entity.id it.version shouldBe entity.version }}
@Testfun testDuplicated() = runTest {
val db = Room.inMemoryDatabaseBuilder<Database>().setDriver(BundledSQLiteDriver()).build()
val entity = Document(title = "Tips & Tricks", text = "Short insights to help you work smarter.") db.document().insert(entity)
val copy = entity.copy(text = "This is different!")
shouldThrow<SQLiteException> { db.document().insert(copy) }.message!!.contains("UNIQUE constraint failed")
}Si intentes inserir entitats amb la mateixa clau dues vegades, la funció anotada amb @Insert llançarà una SQLiteException.
Índexs
La teva clau primària s’indexa automàticament per SQLite. No obstant això, potser vulguis configurar altres índexs per a altres columnes o col·leccions de columnes, per accelerar les consultes.
Per fer-ho, tens dues opcions:
- Utilitza la propietat
indicesa@Entity. Aquesta propietat pren una llista d’anotacionsIndeximbricades, cadascuna de les quals declara un índex. - Utilitza la propietat
indexa@ColumnInfo, per afegir un índex en una única propietat.
La segona és més simple; la primera gestiona escenaris més complexos (p. ex., un índex que involucra múltiples propietats).
Aquí tenim una entitat amb un índex en una propietat category:
@Entity(tableName = "Document")data class Document( @PrimaryKey val id: String = Uuid.random().toString(), val title: String, @ColumnInfo(index = true) val category: String, val text: String? = null, val version: Int = 1,) { @Dao interface SQL {
@Query("SELECT * FROM Document") suspend fun select(): List<Document>
@Query("SELECT * FROM Document WHERE category = :category") suspend fun selectWhereCategory(category: String): List<Document>
@Insert suspend fun insert(vararg indexed: Document) }}Room afegirà l’índex sol·licitat:
CREATE TABLE IF NOT EXISTS Document (id TEXT NOT NULL, title TEXT NOT NULL, category TEXT NOT NULL, text TEXT, version INTEGER NOT NULL, PRIMARY KEY(id))CREATE INDEX IF NOT EXISTS index_document_category ON document (category)Alternativament, pots utilitzar indices a l’anotació @Entity:
@Entity(tableName = "Document", indices = [Index("category")])data class Indexed( @PrimaryKey val id: String = Uuid.random().toString(), val title: String, val category: String, val text: String? = null, val version: Int = 1,)Si tens un índex compost, format per dos o més camps, l’anotació imbricada Index pren una llista de noms de columnes delimitada per comes i generarà l’índex compost.
L’índex serà utilitzat automàticament per SQLite si executes consultes que involucren l’índex. La funció selectWhereCategory() consulta sobre la propietat indexada category, i per tant el nostre índex hauria de ser utilitzat quan cridem aquesta funció:
@Testfun test() = runTest {
val db = Room.inMemoryDatabaseBuilder<IndexedTest.Database>().setDriver(BundledSQLiteDriver()).build()
db.document().insert( Document( title = "What’s New This Week", category = "news", text = "Highlights, fixes, and small delights you might've missed." ) )
db.document().selectWhereCategory("news").let { it.size shouldBe 1 it.first().title shouldBe "What’s New This Week" }}Si l’índex també ha d’imposar unicitat — només una entitat pot tenir el valor indexat — afegeix unique = true a l’anotació Index.
Això requereix que assignis la(les) columna(es) per a l’índex a la propietat value, a causa de la manera com funcionen les anotacions a Kotlin:
@Entity(tableName = "Document", indices = [Index(value = ["title"], unique = true)])data class Indexed( @PrimaryKey val id: String = Uuid.random().toString(), val title: String, @ColumnInfo(index = true) val category: String, val text: String? = null, val version: Int = 1,)Això fa que Room afegeixi la paraula clau UNIQUE a la instrucció CREATE INDEX:
CREATE UNIQUE INDEX IF NOT EXISTS index_document_title ON document (title)Mentre que un índex regular admet múltiples valors, un índex únic no ho fa, portant novament a una SQLiteConstraintException si intentem inserir un duplicat:
@Testfun testDuplicated() = runTest {
val db = Room.inMemoryDatabaseBuilder<IndexedTest.Database>().setDriver(BundledSQLiteDriver()).build()
db.document().insert( Document( title = "Kotlin", category = "news", text = "Kotlin 2.20 is out!" ) )
shouldThrow<SQLiteException> { db.document().insert( Document( title = "Kotlin", category = "news", text = "Kotlin 2.21 is out!" ) ) }.message!!.contains("UNIQUE constraint failed")}Columna personalitzada
Més enllà d’especificar index = true, pots configurar altres opcions en una anotació @ColumnInfo.
Nom
Per defecte, Room generarà noms per a les teves taules i columnes basant-se en els noms de les classes d’entitat i els noms de propietat. En general, fa una feina respectable amb això, i per tant pots deixar-los estar. No obstant això, pots trobar que necessites controlar aquests noms, particularment si estàs intentant coincidir amb un esquema de base de dades existent (p. ex., estàs migrant una aplicació Android existent per utilitzar Room en lloc d’utilitzar SQLite directament). I per a noms de taula en particular, establir el teu propi nom pot simplificar part del SQL que has d’escriure per a funcions anotades amb @Query.
Com hem vist, per controlar el nom de la taula, utilitza la propietat tableName a l’atribut @Entity, i dóna-li un nom de taula SQLite vàlid. Per canviar de nom una columna, afegeix l’anotació @ColumnInfo a la propietat, amb una propietat name que proporcioni el teu nom desitjat per a la columna:
@Entity(tableName = "Person")data class Person( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "firstName") val name: String, @ColumnInfo(name = "lastName") val surname: String,) { @Dao interface SQL { @Query("SELECT * FROM Person") suspend fun select(): List<Person>
@Query("SELECT * FROM Person WHERE firstName = :name") suspend fun selectWhereName(name: String): List<Person>
@Insert suspend fun insert(person: Person) }}Aquí hem canviat la columna de la propietat name a firstName, juntament amb l’especificació del nom de la taula.
El SQL reflectirà aquest canvi:
CREATE TABLE IF NOT EXISTS Person (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, firstName TEXT NOT NULL, lastName TEXT, PRIMARY KEY(id))… encara que ens referim a la propietat pel seu nom Kotlin regular:
db.person().insert(Person(name = "David", surname = "de Mingo"))db.person().selectWhereName("David").first().let { it.surname shouldBe "de Mingo"}Tingues en compte, però, que molts dels atributs d’anotació que utilitza Room es refereixen a noms de columna, no a noms de propietat.
Fixa’t que en el @Query de la consulta selectWhereName() no s’utilitza el nom de la propietat name, sinó el nom de la columna firstName:
@Query("SELECT * FROM Person WHERE firstName = :name")suspend fun selectWhereName(name: String): List<CustomColumn>Altres opcions de @ColumnInfo
Més enllà d’especificar el nom de columna a utilitzar, pots configurar altres opcions en una anotació @ColumnInfo. Hem vist l’ús d’index = true anteriorment per afegir un índex a una columna, però tenim opcions més enllà d’això també.
Collate
Pots especificar una propietat collate per indicar la seqüència de col·lació a aplicar a aquesta columna. Aquí, “seqüència de col·lació” és una manera elegant de dir “funció de comparació per comparar dos strings”.
Hi ha quatre opcions:
BINARYiUNDEFINED, que són equivalents, el valor per defecte, i indiquen que les majúscules són sensiblesNOCASE, que indica que les majúscules no són sensibles (més acuradament, que les 26 lletres de l’alfabet anglès es converteixen a majúscules)RTRIM, que indica que els espais finals haurien de ser ignorats en una col·lació sensible a majúscules
No hi ha cap equivalent UTF complet de NOCASE a SQLite.
Afinitat de tipus
Normalment, Room determinarà el tipus a utilitzar a la columna a SQLite basant-se en el tipus de la propietat (p. ex., les propietats Int creen columnes INTEGER).
Si, per alguna raó, vols intentar anul·lar aquest comportament, pots utilitzar la propietat typeAffinity a @ColumnInfo per especificar algun altre tipus a utilitzar.
Valors per defecte
@ColumnInfo també té una propietat defaultValue. Com pots endevinar pel nom, proporciona un valor per defecte per a la columna a la definició de la taula.
No obstant això, “tal qual”, pot ser menys útil del que penses. Si fas un @Insert d’una entitat, s’utilitzarà el valor per a aquesta columna de l’entitat, no el valor per defecte.
Explorarem defaultValue, i escenaris on és útil, més endavant Valors per defecte i entitats parcials