El Arsenal de Android – Base de datos

Fuera del cuarto

OutOfRoom es una capa de abstracción de base de datos desarrollada y utilizada para reemplazar el código escrito con ROOM ORM de mis proyectos. Esta biblioteca no es un ORM y no pretende convertirse en uno. Es solo una herramienta simple para mantener el código de persistencia limpio y organizado sin ORM.

¿Cómo?

Dejé de usar ORM. Acelera el desarrollo inicial, pero en proyectos grandes, los ORM se convierten en un cuello de botella, por lo que es necesario realizar demasiados trucos si utiliza una funcionalidad SQL muy específica. Los ORM también evitan que el desarrollador tenga que escribir un código de adaptación entre el paradigma relacional y el paradigma orientado a objetos. No utilizar un ORM producirá (algunos) códigos estándar mínimos, sin embargo, los beneficios de flexibilidad son enormes. Puede encontrar numerosos artículos y opiniones sobre esto en línea.

Objetivos de la biblioteca:

  • Ser lo más simple posible al tiempo que proporciona un enfoque limpio para la persistencia de datos.
  • desacoplar el esquema de la base de datos de los modelos (alejarse de las anotaciones en los modelos)
  • capacidad para utilizar el sistema SQLite o la última versión de SQLite proporcionada por requery
  • proporcionando la máxima flexibilidad al desarrollador

¿Cómo importar?

allprojects {
    repositories {
        maven { url 'https://maven.andob.info/repository/open_source/' }
    }
}

Para usar con el sistema SQLite:

dependencies {
    implementation 'ro.andob.outofroom:common-ddl:1.1.4'
    implementation 'ro.andob.outofroom:common-dml:1.1.4'
    implementation 'ro.andob.outofroom:common-query-builder:1.1.4'
    implementation 'ro.andob.outofroom:binding-system-sqlite:1.1.4'
}

Para usar con la última versión de SQLite proporcionada por requery:

dependencies {
    implementation 'ro.andob.outofroom:common-ddl:1.1.4'
    implementation 'ro.andob.outofroom:common-dml:1.1.4'
    implementation 'ro.andob.outofroom:common-query-builder:1.1.4'
    implementation 'ro.andob.outofroom:binding-latest-sqlite:1.1.4'
    implementation 'com.github.requery:sqlite-android:3.35.5'
    implementation 'androidx.sqlite:sqlite-ktx:2.1.0'
}

Definir los modelos

Defina sus plantillas como simples POJO, sin anotaciones:

data class Note
(
    val id : String,
    val title : String,
    val contents : String,
    val color : NoteColor,
)
enum class NoteColor(val id : Int)
{
    White(1),
    Yellow(2),
    Green(3),
}

Definición de esquema de base de datos

class NotesDatabaseSchema : Schema()
{
    val noteTable = NoteTable()
    class NoteTable : Table(name = "Note")
    {
        val id = Column(name = "id", type = SQLType.Text, notNull = true)
        val title = Column(name = "title", type = SQLType.Text)
        val contents = Column(name = "contents", type = SQLType.Text)
        val color = Column(name = "color", type = SQLType.Integer)

        override val primaryKey get() = PrimaryKey(id)
        override val foreignKeys get() = listOf<ForeignKey>()
    }
    
    //todo more tables...

    override val indices get() = listOf<Index>(
        Index(table = noteTable, column = noteTable.title),
        Index(table = noteTable, column = noteTable.contents)
    )
}

Referencia de API:

  • Schema class representa el esquema de la base de datos, que contiene tablas e índices. Esta clase también tiene una propiedad tables que devolverá una lista de todas las tablas definidas dentro.
  • Table class representa una tabla de base de datos, que contiene columnas, claves primarias y externas. Las clases de tabla tienen una propiedad columns que devolverá una lista de todas las columnas dentro de la tabla. También hay un método toCreateTableSQL que devuelve una cadena con el equivalente create table ... Comando SQL.
  • Column clase representa una columna de una tabla. Las columnas se definen por su nombre y tipo. De forma predeterminada, las columnas son anulables, pero puede agregar notNull = true.
  • SQLType enum contiene definiciones de tipo SQLite: Integer, Real, Text, Blob.
  • PrimaryKey puede ser simple: PrimaryKey(id), compuesto: PrimaryKey(id, name) o simple con autoincremento: PrimaryKey.AutoIncrement(id)
  • Index clase representa un índice de tabla. Los índices también pueden ser únicos (solo agregue unique = true a la definición del índice para hacerlo único). También hay un método toCreateIndexSQL que devuelve una cadena con el equivalente create index ... Comando SQL.

Definir el ayudante para abrir la base de datos.

class NotesDatabaseOpenHelper
(
    private val schema : NotesDatabaseSchema
) : SQLiteOpenHelper
(
    /*context*/ App.context,
    /*name*/ "notes.db",
    /*cursorFactory*/ null,
    /*version*/ 1,
    /*onCorruption*/ { throw Error("Detected a corrupt database!") }
)
{
    override fun onCreate(db : SQLiteDatabase)
    {
        for (table in schema.tables)
            db.execSQL(table.toCreateTableSQL())

        for (index in schema.indices)
            db.execSQL(index.toCreateIndexSQL())
    }

    override fun onConfigure(db : SQLiteDatabase)
    {
        super.onConfigure(db)
        db.enableWriteAheadLogging()
    }

    override fun onUpgrade(db : SQLiteDatabase?, oldVersion : Int, newVersion : Int)
    {
        //todo to migrate, use a migration manager such as Flyway or write your own
    }
}

Definición del objeto contenedor / base de datos DAO

object NotesDatabase
{
    private val openHelper = NotesDatabaseOpenHelper(schema)

    private val entityManager get() = openHelper.toEntityManager()
    private val schema get() = NotesDatabaseSchema()

    fun noteDao() = NoteDao(schema, entityManager)
    //more DAOs
}

Defina clases DAO similares a las definidas con ROOM.

class NoteDao
(
    private val schema : NotesDatabaseSchema,
    private val entityManager : EntityManager,
)
{
    //DAO methods...
}

Definición de métodos de adaptador dentro de DAO

Debe definir los métodos de adaptador que convertirán:

  • populateInsertData(insertData : InsertData, note : Note) desde el modelo hasta los datos a introducir.
  • parseQueryResult(queryResult : QueryResult) : Note a partir de los datos resultantes de la consulta en el modelo. Un objeto QueryResult será equivalente a una fila de una tabla de resultados de consulta.
class NoteDao
(
    private val schema : NotesDatabaseSchema,
    private val entityManager : EntityManager,
)
{
    private fun populateInsertData(insertData : InsertData, note : Note)
    {
        insertData.putString(schema.noteTable.id, note.id)
        insertData.putString(schema.noteTable.title, note.title)
        insertData.putString(schema.noteTable.contents, note.contents)
        insertData.putNoteColor(schema.noteTable.color, note.color)
    }

    private fun parseQueryResult(queryResult : QueryResult) : Note
    {
        return Note(
            id = queryResult.getString(schema.noteTable.id)!!,
            title = queryResult.getString(schema.noteTable.title)?:"",
            contents = queryResult.getString(schema.noteTable.contents)?:"",
            color = queryResult.getNoteColor(schema.noteTable.color),
        )
    }
}

Las clases InsertData y QueryResult tienen métodos getter / setter como:

InsertData:
fun putBoolean(column : Column, value : Boolean?) { ... }
fun putDouble(column : Column, value : Double?) { ... }
fun putFloat(column : Column, value : Float?) { ... }
fun putInt(column : Column, value : Int?) { ... }
fun putLong(column : Column, value : Long?) { ... }
fun putString(column : Column, value : String?) { ... }

QueryResult:
fun getBoolean(column : Column) : Boolean { ... }
fun getDouble(column : Column) : Double? { ... }
fun getFloat(column : Column) : Float? { ... }
fun getInt(column : Column) : Int? { ... }
fun getLong(column : Column) : Long? { ... }
fun getString(column : Column) : String? { ... }

//converts first cell of the row (QueryResult) into:
fun toBoolean() : Boolean { ... }
fun toDouble() : Double { ... }
fun toFloat() : Float { ... }
//...

Por supuesto, no todos los tipos de campos de clase son primitivos o cadenas. Puede definir “FieldAdapters” personalizados simplemente creando métodos de extensión en InsertData Y QueryResult clases. Esto sería equivalente a los métodos de conversión de SALA:

//file NotesDatabaseFieldAdapters.kt
private fun findNoteColor(id : Int) : NoteColor? = NoteColor.values().find { it.id==id }
fun InsertData.putNoteColor(column : Column, noteColor : NoteColor) = putInt(column, noteColor.id)
fun QueryResult.getNoteColor(column : Column) = getInt(column)?.let(::findNoteColor)?:NoteColor.White

DAO – entrada / actualización de datos

Usar entityManager.insert() para ingresar o actualizar datos:

class NoteDao
(
    private val schema : NotesDatabaseSchema,
    private val entityManager : EntityManager,
)
{
    fun insert(note : Note, or : InsertOr = InsertOr.Fail)
    {
        entityManager.insert(or = or,
            table = schema.noteTable,
            columns = schema.noteTable.columns,
            adapter = { insertData -> populateInsertData(insertData, note) })
    }

    fun update(note : Note) =
        insert(note, or = InsertOr.Replace)

    private fun populateInsertData(insertData : InsertData, note : Note) ...
    private fun parseQueryResult(queryResult : QueryResult) : Note ...
}

Uso:

val note = Note(
    id = UUID.randomUUID().toString(),
    title = "test", contents = "test",
    color = NoteColor.White)

NotesDatabase.noteDao().insert(note)
NotesDatabase.noteDao().update(note)

DAO: ejecución de comandos SQL

Usar entityManager.exec() para ejecutar sentencias SQL que no tienen resultado.

class NoteDao
(
    private val schema : NotesDatabaseSchema,
    private val entityManager : EntityManager,
)
{
    fun delete(note : Note)
    {
        entityManager.exec(
            sql = """delete from ${schema.noteTable}
                     where ${schema.noteTable.id} = ?""",
            arguments = arrayOf(note.id))
    }
    
    fun deleteAll()
    {
        entityManager.exec("delete from ${schema.noteTable}")
    }

    private fun populateInsertData(insertData : InsertData, note : Note) ...
    private fun parseQueryResult(queryResult : QueryResult) : Note ...
}

Nota: Para pasar argumentos, use la sintaxis de declaración preparada (? Dentro de la consulta, luego arguments = arrayOf(...)). Esto es útil por razones de seguridad, para evitar la inyección de SQL.

Uso:

NotesDatabase.noteDao().delete(note)
NotesDatabase.noteDao().deleteAll()

DAO – recuperación de datos

Usar entityManager.query para ejecutar sentencias SQL que tengan un resultado:

class NoteDao
(
    private val schema : NotesDatabaseSchema,
    private val entityManager : EntityManager,
)
{
    fun getAll() : List<Note>
    {
        return entityManager.query(
            sql = "select * from ${schema.noteTable}",
            adapter = ::parseQueryResult)
    }

    fun getById(noteId : String) : Note?
    {
        return entityManager.query(
            sql = """select * from ${schema.noteTable}
                     where ${schema.noteTable.id} = ?
                     limit 1""",
            arguments = arrayOf(noteId),
            adapter = ::parseQueryResult
        ).firstOrNull()
    }

    fun getByIds(noteIds : List<String>) : List<Note>
    {
        return entityManager.query(
            sql = """select * from ${schema.noteTable}
                     where ${schema.noteTable.id} in (${questionMarks(noteIds)})""",
            arguments = noteIds.toTypedArray(),
            adapter = ::parseQueryResult)
    }
    
    fun count() : Int
    {
        return entityManager.query(
            sql = "select count(*) from ${schema.noteTable}",
            adapter = { queryResult -> queryResult.toInt() }
        ).firstOrNull()?:0
    }

    private fun populateInsertData(insertData : InsertData, note : Note) ...
    private fun parseQueryResult(queryResult : QueryResult) : Note ...
}

Nota: Para pasar argumentos, use la sintaxis de declaración preparada (? Dentro de la consulta, luego arguments = arrayOf(...)). Esto es útil por razones de seguridad, para evitar la inyección de SQL.

Nota: para pasar una lista de temas (como en where .. in .. cláusula), use la sintaxis de declaración preparada (questionMarks () para generar una cadena de?,?,? …, luego arguments = ...). Esto es útil por razones de seguridad, para evitar la inyección de SQL.

Uso:

val allNotes = NotesDatabase.noteDao().getAll()
val someNote = NotesDatabase.noteDao().getById(note.id)
val someNotes = NotesDatabase.noteDao().getByIds(listOf(note.id))
val noteCount = NotesDatabase().noteDao().count()

DAO: usando el generador de consultas

Esta biblioteca también contiene un “Generador de consultas”, un clon de mi biblioteca. HABITACIÓN-Dynamic-Dao. Con él, puede convertir objetos de filtro en comandos SQL seleccionados. Por favor lea el tutorial de HABITACIÓN-Dynamic-Dao documentación. La sintaxis es muy similar:

data class NoteFilter
(
    override val limit : Int,
    override val offset : Int,
    override val search : String?
) : IQueryBuilderFilter
class NoteDao
(
    private val schema : NotesDatabaseSchema,
    private val entityManager : EntityManager,
)
{
    fun getFiltered(noteFilter : NoteFilter) : List<Note>
    {
        return entityManager.query(
            sql = NoteQueryBuilder(schema, noteFilter).build(),
            adapter = ::parseQueryResult)
    }

    private fun populateInsertData(insertData : InsertData, note : Note) ...
    private fun parseQueryResult(queryResult : QueryResult) : Note ...
}
class NoteQueryBuilder
(
    private val schema : NotesDatabaseSchema,
    filter : NoteFilter,
) : QueryBuilder<NoteFilter>(filter)
{
    override fun table() = schema.noteTable

    override fun where(conditions : QueryWhereConditions) : String
    {
        if (filter.search != null)
        {
            conditions.addSearchConditions(
                search = filter.search, columns = arrayOf(
                    schema.noteTable.title,
                    schema.noteTable.contents,
                ))
        }

        return conditions.mergeWithAnd()
    }
}
val notes = NotesDatabase.noteDao().getFiltered(NoteFilter(search = "test", limit = 100, offset = 0))

Usando el sistema SQLite versus el último SQLite de Requery

Para usar esta biblioteca con el sistema SQLite (la biblioteca SQLite incluida en el sistema operativo Android), simplemente importe los componentes relevantes:

    implementation 'ro.andob.outofroom:binding-system-sqlite:1.1.4'
import ro.andob.outofroom.system_sqlite.toEntityManager
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

Para usar esta biblioteca con la última versión de SQLite proporcionada por requery, simplemente importe los componentes relevantes:

    implementation 'ro.andob.outofroom:binding-latest-sqlite:1.1.4'
import ro.andob.outofroom.latest_sqlite.toEntityManager
import io.requery.android.database.sqlite.SQLiteDatabase
import io.requery.android.database.sqlite.SQLiteOpenHelper

Con la biblioteca de compatibilidad de consultas de SQLite, se incluirá una versión de la biblioteca SQLite con su aplicación. Esto producirá un tamaño de APK más grande. Ventajas de usar la última versión de SQLite: velocidad, correcciones de seguridad, todos los usuarios de su aplicación usarán exactamente la misma versión de SQLite en una amplia gama de dispositivos.

Migración de ROOM

Esta biblioteca no proporciona una herramienta automática para migrar desde ROOM. La forma recomendada de migrar es:

  • Escriba pruebas unitarias en todo el nivel de persistencia (en TODOS los métodos de todos los DAO)
  • Reemplace ROOM con OutOfRoom, reescriba el código manteniendo intacta la API de DAO (firmas de métodos)
  • Ejecute pruebas unitarias de persistencia nuevamente, corrija errores

Licencia

Copyright 2021 Andrei Dobrescu

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

.

Compruebe también

en vivo desde Droidcon, incluida la mayor actualización de Gemini en Android Studio y más lanzamientos del SDK de Android.

Acabamos de lanzar nuestro episodio de otoño de #TheAndroidShow en YouTube etcétera desarrollador.android.comy esta vez …

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *