Crear Vistas En Room

En este tutorial aprenderás a cómo usar la anotación @DatabaseView para crear vistas en Room con el fin de empaquetar consultas complejas.

Recuerda leer el tutorial de relaciones muchos a muchos para que sigas la secuencia de esta guía de Room.


Vistas En SQLite

Las vistas son comandos SELECT empaquetados bajo un nombre dado, que proveen acceso directo a un conjunto de datos de interés.

Las usamos para:

  • Obtener información con estructura amigable
  • Ocultar los detalles del esquema de la base de datos
  • Resumir reportes de consultas que impliquen la cláusula JOIN

La librería Room te permite asociar clases de resultados a la creación de una vista para que puedas consultarlas a través de DAOs.

Para crear una vista, añade una nueva clase que represente el resultado y anótala con @DatabaseView. Usa el parámetro value para establecer comando SELECT asociado y viewName si deseas establecer un nombre particular para la vista

@DatabaseView(
        value= 
        "SELECT columna1, columna2, ... " +
        "FROM nombre_tabla " +
        "WHERE [condicion]", viewName="example_view")
public class ViewExample {
    public tipo column1;
    public tipo column2;
    ...
}

Luego inclúyela en tu base de datos con la propiedad views de la anotación @Entity.

@Database(entities = {Example.class}, views = {ViewExample.class},
          version = 1)
public abstract class ExampleDatabase extends RoomDatabase {
    public abstract ExampleDao exampleDao();
}

En el momento en que declares la sentencia SELECT haz que coincidan los nombres de los campos de la clase con los nombres de la consulta.

Usa la anotación @ColumnInfo si deseas nombrar el atributo con otra convención.


Ejemplo De Vistas En Room

Tomemos la actividad con la listas de compras de nuestro App de ejemplo para añadir una vista a la base de datos.

Prototipo de App listas de compras con cantidad de ítems

Con esta vista mostraremos todas las columnas de la lista de compras que ya obteníamos junto a la cantidad de ítems que contiene.

El siguiente es el código de SQLite puro de la vista.

SELECT
    l.*,    
    i.created_date,    
    COUNT(*) as itemsCount 
 FROM
    shopping_list l 
    INNER JOIN
       info i USING(shopping_list_id) 
    INNER JOIN
       shopping_list_item USING(shopping_list_id) 
    INNER JOIN
       item USING(item_id)
 GROUP BY
    l.shopping_list_id

Recuerda que USING equivale a ON tabla1.id_tabla1 = tabla2.id_tabla1 si ambas columnas de comparación se llaman igual.

Puedes descargar el proyecto Android Studio desde el siguiente enlace:

1. Crear Vista En La Base De Datos

Crea una nueva clase llamada ShoppingListView la cual use como sentencia de selección la consulta de todas las listas de compras y el conteo de sus ítems.

@DatabaseView(
        value = "SELECT l.*, i.created_date, COUNT(*) as itemsCount " +
                "FROM shopping_list l " +
                "INNER JOIN info i " +
                "USING(shopping_list_id) " +
                "INNER JOIN shopping_list_item " +
                "USING(shopping_list_id) " +
                "INNER JOIN item " +
                "USING(item_id)" +
                "GROUP BY l.shopping_list_id",
        viewName = "v_full_shopping_lists"
)
public class ShoppingListView {
    @ColumnInfo(name = "shopping_list_id")
    public String id;

    public String name;

    public String category;

    @ColumnInfo(name = "is_favorite")
    public boolean favorite;

    @ColumnInfo(name = "created_date")
    public String createdDate;

    public int itemsCount;
}

Inmediatamente regístrala en ShoppingListDatabase y sube la versión a 7.

@Database(entities = {
        ShoppingList.class,
        Info.class,
        Collaborator.class,
        Item.class,
        ShoppingListItem.class},
        views = ShoppingListView.class,
        version = 7, exportSchema = false)
public abstract class ShoppingListDatabase extends RoomDatabase {
}

2. Consultar Vista Con DAO

Al interior de ShoppingListDao modifica a los dos métodos que retornan las listas de compras para el adaptador (consulta general y filtro categorías).

La idea es que sus anotaciones @Query consulten a la vista que creaste.

@Transaction
@Query("SELECT * FROM v_full_shopping_lists")
public abstract LiveData<List<ShoppingListWithCollaborators>> shoppingLists();

@Transaction
@Query("SELECT * FROM v_full_shopping_lists WHERE category IN(:categories)")
public abstract LiveData<List<ShoppingListWithCollaborators>> getShoppingListsByCategories(List<String> categories);

3. Actualizar Clase De Resultados

Ya que cambiarás la proyección de la consulta de listas de compras, es necesario que modifiques la clase ShoppingListWithCollaborators. Usa la vista como entidad padre y tan solo deja a la tabla de colaboradores.

public class ShoppingListWithCollaborators {
    @Embedded
    public ShoppingListView shoppingList;

    @Relation(
            entity = Collaborator.class,
            parentColumn = "shopping_list_id",
            entityColumn = "shopping_list_id",
            projection = {"name"}
    )
    public List<String> collaboratorNames;
}

Con esto obtendrás toda la información necesaria en el diseño de MainActivity.

4. Modificar Layout De Listas De Compras

El cambio es mínimo. Agrega un TextView que represente la cantidad de ítems en el extremo inferior derecho.

Lista de compras con cantidad de ítems

Usa la siguiente definición XML:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_marginBottom="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    android:layout_height="wrap_content">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="144dp"
        android:padding="@dimen/normal_padding">

        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?attr/textAppearanceHeadline6"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/favorite_button"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0"
            tools:text="Lista de ejemplo" />

        <TextView
            android:id="@+id/created_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?textAppearanceCaption"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/name"
            app:layout_constraintVertical_bias="0.0"
            tools:text="26/05/2020 01:12:54" />

        <com.google.android.material.checkbox.MaterialCheckBox
            android:id="@+id/favorite_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginRight="8dp"
            android:button="@drawable/sl_favorite_24"
            android:minWidth="0dp"
            android:minHeight="0dp"
            app:buttonTint="@color/favorite_color"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/delete_button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />

        <ImageView
            android:id="@+id/delete_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0"
            app:srcCompat="@drawable/ic_delete_24" />

        <TextView
            android:id="@+id/collaborators_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/collaborators_label"
            android:textAllCaps="true"
            android:textAppearance="?textAppearanceCaption"
            app:layout_constraintBottom_toTopOf="@+id/collaborator_names"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/created_date"
            app:layout_constraintVertical_bias="1.0" />

        <TextView
            android:id="@+id/collaborator_names"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?textAppearanceBody1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Cesar, Ramiro, Cristina" />

        <TextView
            android:id="@+id/items_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="1.0"
            android:textAppearance="?textAppearanceHeadline4"
            tools:text="5" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

Y en consecuencia, abre el adaptador y bindea el nuevo valor.

public class ShoppingListViewHolder extends RecyclerView.ViewHolder {
    private final TextView mNameText;
    private final CheckBox mFavoriteButton;
    private final ImageView mDeleteButton;
    private final TextView mCreatedDateText;
    private final TextView mCollaboratorsText;
    private final TextView mItemsCount;

    public ShoppingListViewHolder(@NonNull View itemView) {
        super(itemView);
        mNameText = itemView.findViewById(R.id.name);
        mCreatedDateText = itemView.findViewById(R.id.created_date);
        mFavoriteButton = itemView.findViewById(R.id.favorite_button);
        mDeleteButton = itemView.findViewById(R.id.delete_button);
        mCollaboratorsText = itemView.findViewById(R.id.collaborator_names);
        mItemsCount = itemView.findViewById(R.id.items_count);

        // Setear eventos
        mFavoriteButton.setOnClickListener(this::manageEvents);
        mDeleteButton.setOnClickListener(this::manageEvents);
        itemView.setOnClickListener(this::manageEvents);
    }

    private void manageEvents(View view) {
        if (mItemListener != null) {
            ShoppingListWithCollaborators clickedItem = mShoppingLists.get(getAdapterPosition());

            // Manejar evento de click en Favorito
            if (view.getId() == R.id.favorite_button) {
                mItemListener.onFavoriteIconClicked(clickedItem);
                return;
            } else if (view.getId() == R.id.delete_button) {
                mItemListener.onDeleteIconClicked(clickedItem);
                return;
            }

            mItemListener.onClick(clickedItem);
        }
    }

    public void bind(ShoppingListWithCollaborators item) {
        mNameText.setText(item.shoppingList.name);
        mFavoriteButton.setChecked(item.shoppingList.favorite);
        mCreatedDateText.setText(item.shoppingList.createdDate);
        mCollaboratorsText.setText(TextUtils.join(",", item.collaboratorNames));
        mItemsCount.setText(String.valueOf(item.shoppingList.itemsCount));
    }
}

Con esos cambios ya puedes ejecutar el proyecto y ver la consulta de la view creada en la base de datos.

Crear Vistas En Room

Siguiente Tutorial: Database Inspector

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!