Relaciones Muchos A Muchos Con Room

En este tutorial aprenderás a implementar relaciones muchos a muchos con Room a través de la anotación @Relation.

Recuerda leer el tutorial relaciones uno a muchos para seguir un trayecto secuencial de esta guía de Room.

Implementar Relaciones Muchos A Muchos

Este tipo de relaciones se dan cuando múltiples instancias de una entidad están asociados con múltiples instancias de otra entidad.

Si deseas simplificar la consulta de estas relaciones sin usar queries complejas, entonces puedes implementarla de la siguiente forma:

Paso 1: Crear una entidad asociativa (referencia cruzada) que contenga las claves primarias de las tablas de la relación.

@Entity(primaryKeys = {"aId", "bId"})
public class ABCrossRef {
    public int aId;
    public int bId;
}

Esta tabla te resulta de la conversión relacional en una relación muchos a muchos, por lo que le debes dar persistencia en tu base de datos SQLite.

Paso 2: Crea una clase de resultado dependiendo de la dirección de consulta. Es decir, si quieres obtener todas las filas B asociadas a una entidad A, o si quieres obtener todas las filas A asociadas a una entidad B.

public class AWithBs {
    @Embedded
    public A a;

    @Relation(
            parentColumn = "aId",
            entityColumn = "bId",
            associateBy = @Junction(ABCrossRef.class)
    )
    public List<B> bs;
}

public class BWithAs {
    @Embedded
    public B b;

    @Relation(
            parentColumn = "bId",
            entityColumn = "aId",
            associateBy = @Junction(ABCrossRef.class)
    )
    public List<A> as;
}

Usa la propiedad associateBy para identificar la entidad de asociación con la anotación @Junction.

Paso 3: Agrega un método de consulta al DAO y dependiendo de la dirección de la misma, usa la entidad de relación apropiada como tipo de retorno.

@Transaction
@Query("SELECT * FROM a")
public List<A> getAWithBs();

@Transaction
@Query("SELECT * FROM b")
public List<B> getBWithAs();

Ejemplo De Relaciones Muchos A Muchos Con Room

Usaremos la relación entre las tablas shopping_list e items en nuestro ejemplo de App de listas de compras, para ilustrar la implementación de una relación muchos a muchos.

Modelo relacional muchos a muchos con Room

Luego mostraremos los ítems de una lista de compras en un RecyclerView, ubicado en la actividad la pantalla de edición.

Prototipo actividad de edición de lista de compras

Descarga el código completo para tenerlo como referencia desde el siguiente enlace:

1. Crear Tabla Para Ítems

Crea una nueva clase anotada con @Entity cuyo nombre refleje el diagrama de base de datos anterior (item).

@Entity
public class Item {
    @NonNull
    @PrimaryKey
    @ColumnInfo(name = "item_id")
    public String id;

    @NonNull
    public String name;

    public Item(@NonNull String id, @NonNull String name) {
        this.id = id;
        this.name = name;
    }
}

2. Crear Tabla Asociativa

Crea otra clase para representar la tabla asociativa shopping_list_item. Como viste en el diagrama, agrega las claves primarias de las tablas involucradas y márcalas como clave primaria compuesta.

@Entity(tableName = "shopping_list_item",
        primaryKeys = {"shopping_list_id", "item_id"},
        foreignKeys = {
                @ForeignKey(
                        entity = ShoppingList.class,
                        parentColumns = "shopping_list_id",
                        childColumns = "shopping_list_id",
                        onDelete = ForeignKey.CASCADE),
                @ForeignKey(
                        entity = Item.class,
                        parentColumns = "item_id",
                        childColumns = "item_id")
        }
)
public class ShoppingListItem {
    @NonNull
    @ColumnInfo(name = "shopping_list_id")
    public String shoppingListId;

    @NonNull
    @ColumnInfo(name = "item_id")
    public String itemId;

    public ShoppingListItem(@NonNull String shoppingListId, @NonNull String itemId) {
        this.shoppingListId = shoppingListId;
        this.itemId = itemId;
    }
}

Luego actualiza la base de datos a la versión 6 y agrega ambas entidades.

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

3. Crear Clase De Resultado

Añade la clase para mapear los resultados obtenidos de la relación.

Recuerda que consultaremos los ítems de las listas de compras, por lo que el campo @Embedded es la lista de compra y el @Relation es la lista de items.

public class ShoppingListWithItems {
    @Embedded
    public ShoppingList shoppingList;

    @Relation(
            parentColumn = "shopping_list_id",
            entityColumn = "item_id",
            associateBy = @Junction(ShoppingListItem.class)
    )
    public List<Item> items;
}

4. Obtener Items De Lista De Compra

Ahora modifica ShoppingListDao para que el método que consultaba el detalle de una lista de compras retorne en ShoppingListWithItems.

@Transaction
@Query("SELECT * FROM shopping_list WHERE shopping_list_id = :id")
public abstract LiveData<ShoppingListWithItems> shoppingListWithItems(String id);

Propaga este cambio a los retornos de los métodos del ViewModel y el repositorio.

5. Modificar Layout De Edición De Listas De Compras

Como observaste en el prototipo propuesto, debes agregar un RecyclerView para mostrar los ítems de la lista de compras.

Así que abre activity_edit_shopping_list.xml y pega el siguiente código:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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_height="match_parent"
    tools:context=".editshoppinglist.EditShoppingListActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="0dp"
        android:id="@+id/items_list"
        android:layout_height="0dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        tools:listitem="@layout/list_item"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Seguido, crea un layout para el ítem llamado list_item.xml para mostrar el nombre de cada ítem.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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_height="?listPreferredItemHeight"
    android:padding="@dimen/normal_padding">

    <TextView
        android:id="@+id/item_name"
        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"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Espinaca" />
</androidx.constraintlayout.widget.ConstraintLayout>

6. Crear Adaptador De Items

Infla la lista de items creando un adaptador que enlace la entidad Item con el TextView del layout para items de lista.

Usa el siguiente código:

public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {

    private List<Item> mItems;

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ItemViewHolder(LayoutInflater.from(
                parent.getContext()).inflate(
                R.layout.list_item,
                parent,
                false)
        );
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        Item item = mItems.get(position);
        holder.bind(item);
    }

    @Override
    public int getItemCount() {
        return mItems == null ? 0 : mItems.size();
    }

    public void setItems(List<Item> items) {
        mItems = items;
        notifyDataSetChanged();
    }

    public static class ItemViewHolder extends RecyclerView.ViewHolder {
        public TextView mNameText;

        public ItemViewHolder(@NonNull View itemView) {
            super(itemView);
            mNameText = itemView.findViewById(R.id.item_name);
        }

        public void bind(Item item) {
            mNameText.setText(item.name);
        }
    }
}

7. Cargar Elementos Con El ViewModel

Desde EditShoppingListActivity toma la referencia del recycler view y vincúlalo a una instancia del adaptador.

private void setupItemsList() {
    mItemsList = findViewById(R.id.items_list);
    mAdapter = new ItemAdapter();
    mItemsList.setAdapter(mAdapter);
}

Ahora en la suscripción que hicimos al LiveData con la lista de compras, usa el método setItems() del adaptador para actualizar la lista en cada notificación de cambio.

private void subscribeToUi() {
    mViewModel.getShoppingList().observe(this,
            shoppingList -> {
                mActionBar.setTitle(shoppingList.shoppingList.name);
                mAdapter.setItems(shoppingList.items);
            }
    );
}

8. Crear DAOs

Añade dos nuevos DAOs abstractos para los ítems (ItemDao) y la tabla de referencia (ShoppingListItemDao). Escríbeles un método para insertar una lista de elementos.

@Dao
public abstract class ItemDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    public abstract void insertAll(List<Item> items);
}

@Dao
public abstract class ShoppingListItemDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    public abstract void insertAll(List<ShoppingListItem> shoppingListItems);
}

Ahora exponlos desde ShoppingListDatabase con un método get.

public abstract ItemDao itemDao();

public abstract ShoppingListItemDao shoppingListItemDao();

9. Llamar A Múltiples DAOs En Una Trasacción

Y para terminar. Ya que vas a insertar datos desde múltiples DAOs desde la prepoblación, es necesario que crees una transacción para conservar la atomicidad.

Ve a base de datos e inserta cinco items por cada lista de compras en la prepoblación y luego llama al método RoomDatabase.runInTransaction().

private static void prepopulate(Context context) {
    // Obtener instancias de Daos
    ShoppingListDao shoppingListDao = INSTANCE.shoppingListDao();
    ItemDao itemDao = INSTANCE.itemDao();
    ShoppingListItemDao shoppingListItemDao = INSTANCE.shoppingListItemDao();

    List<ShoppingListInsert> lists = new ArrayList<>();
    List<Info> infos = new ArrayList<>();
    List<Collaborator> collaborators = new ArrayList<>();
    List<Item> items = new ArrayList<>();
    List<ShoppingListItem> shoppingListItems = new ArrayList<>();

    for (int i = 0; i < 5; i++) {

        String dummyId = String.valueOf((i + 1));

        // Crear lista de compras
        ShoppingListInsert shoppingList = new ShoppingListInsert(
                dummyId,
                "Lista " + (i + 1)
        );

        // Crear info
        String date = Utils.getCurrentDate();
        Info info = new Info(
                shoppingList.id, date, date);

        // Crear colaborador
        Collaborator collaborator = new Collaborator(dummyId,
                "Colaborador " + dummyId, dummyId);

        // Crear ítems de la lista
        for (int j = 0; j < 5; j++) {
            Item item = new Item(dummyId + (j + 1), "Item #" + (j + 1));

            // Crear filas de "lista <contiene> item"
            ShoppingListItem shoppingListItem = new ShoppingListItem(shoppingList.id, item.id);

            items.add(item);
            shoppingListItems.add(shoppingListItem);
        }

        lists.add(shoppingList);
        infos.add(info);
        collaborators.add(collaborator);

    }


    // Crear transacción para llamar DAOs
    getInstance(context).runInTransaction(() -> {
        shoppingListDao.insertAllWithInfosAndCollaborators(lists, infos, collaborators);
        itemDao.insertAll(items);
        shoppingListItemDao.insertAll(shoppingListItems);
    });
}

Por otro lado, como también insertas una lista de compras desde la actividad AddShoppingListActivity, es necesario que modifiques el método de guardado del repositorio.

Entra a ShoppingListRepository y llama al método runInTransaction() de la base de datos para insertar todos los registros desde los DAOs relacionados en insert().

public void insert(ShoppingListInsert shoppingList, Info info,
                   List<Collaborator> collaborators, List<Item> items) {
    ShoppingListDatabase.dbExecutor.execute(
            () -> mDb.runInTransaction(
                    () -> processInsert(shoppingList, info, collaborators, items)
            )
    );
}

private void processInsert(ShoppingListInsert shoppingList, Info info,
                           List<Collaborator> collaborators, List<Item> items) {
    // Insertar lista de compras
    mShoppingListDao.insertWithInfoAndCollaborators(shoppingList, info, collaborators);

    // Insertar items
    mItemDao.insertAll(items);

    // Generar registros de relación
    List<ShoppingListItem> shoppingListItems = new ArrayList<>();
    for (Item item : items) {
        shoppingListItems.add(new ShoppingListItem(shoppingList.id, item.id));
    }

    // Insertar registros de relación
    mShoppingListItemDao.insertAll(shoppingListItems);
}

Finalmente, ejecuta el aplicativo y revisa que se carguen los ítems de las listas de compras en la actividad de edición.

Relaciones Muchos A Muchos Con Room

Siguiente tutorial: Crear Vistas En Room

Ú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!