Relaciones Uno A Muchos Con Room

En este tutorial aprenderás a cómo usar la anotación @Relation para implementar relaciones uno a muchos con Room.

Implementar Relación Uno A Muchos

Una relación uno a muchos se da cuando una instancia de la entidad padre puede relacionarse con uno o más instancias de la entidad hija.

Puedes implementarlas en Room con los siguientes pasos:

Paso 1: Incluye una referencia a la clave primaria de la entidad padre en la entidad hija.

@Entity(tableName = "parent")
public class Parent {
    @NonNull
    @PrimaryKey
    public long id;
}

@Entity(tableName = "child")
public class Child {
    public long parentId;
}

Paso 2: Crea una clase de relación con un campo del tipo del padre anotado con @Embedded. Y otro que sea una lista del tipo de la entidad hija, anotado con @Relation (exactamente igual que en relaciones 1:1):

public class ParentWithChilds{
    @Embedded
    public Parent parent;
    
    @Relation(
            parentColumn="id", 
            entityColumn="parentId"
    )
    public List<Child> childs;
}

Paso 3: Y para consultar los resultados, añade un método anotado con @Query que retorne el tipo de la clase de relación.

@Transaction
@Query("SELECT * FROM parent")
public List<ParentWithChilds> getParentWithChilds();

Proyecciones

Si deseas retornar solo algunas columnas de tu entidad que estén especificadas en un POJO, entonces agrega la propiedad entity especificando la entidad de la que se inferirá:

public class ChildId{
    public long id;
}

public class ParentWithChilds{
    @Embedded
    public Parent parent;

    @Relation(
            parentColumn="id",
            entityColumn="parentId",
            entity = Child.class
    )
    public List<ChildId> childs;
}

También existen las proyecciones desplegables, que consisten en especificar a Room el nombre de las columnas específicas a consultar con la propiedad projection.

public class ParentWithChilds{
    @Embedded
    public Parent parent;

    @Relation(
            parentColumn="id",
            entityColumn="parentId",
            entity = Child.class,
            projection = {"id"}
    )
    public List<Long> childIds;
}

Ejemplo De Relaciones Uno A Muchos Con Room

Tomemos la relación entre las tablas shopping_list y collaborator del ejemplo de la app de listas de compras.

Diagrama uno a muchos con Room

Supón que quieres mostrar el nombre del colaborador en los ítems de la lista de la actividad principal.

Prototipo de relaciones uno a  muchos con Room

Ya que es una relación uno a muchos, veamos la solución aplicando lo aprendido previamente.

Puedes descargar el código completo desde el siguiente enlace:

1. Crear Entidad De Colaboradores

Crea una nueva clase llamada Collaborator. Anótala con @Entity y agrega como campos todas las columnas mostradas en el modelo relacional. También asegura la restricción foránea hacia ShoppingList.

@Entity(tableName = "collaborator",
        foreignKeys = @ForeignKey(
                entity = ShoppingList.class,
                parentColumns = "id",
                childColumns = "shopping_list_id")
)
public class Collaborator {
    @NonNull
    @PrimaryKey
    public String id;

    public String name;

    @ColumnInfo(name = "shopping_list_id")
    public String shoppingListId;

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

Luego añade la entidad a la lista de @Database y aumenta la versión a 5.

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

2. Relacionar ShoppingList Y Collaborator

Crea otra clase para la relación de la lista de compras y colaboradores llamada ShoppingListWithCollaborators.

Ya que deseas solo el nombre del colaborador en el resultado, aplica una proyección desplegable con la columna collaborator.name.

Y no olvides incluir la relación 1:1 con Info que creaste en el tutorial anterior.

public class ShoppingListWithCollaborators {
    @Embedded
    public ShoppingListForList shoppingList;

    @Relation(
            entity = Collaborator.class,
            parentColumn = "id",
            entityColumn = "shopping_list_id",
            projection = {"name"}
    )

    public List<String> collaboratorNames;
    @Relation(
            entity = Info.class,
            parentColumn = "id",
            entityColumn = "shopping_list_id",
            projection = {"created_date"}
    )
    public String createdDate;
}

3. Obtener Resultados (1:*) En El DAO

Ahora ve a ShoppingListDao y actualiza los métodos getAll() y getShoppinhListsByCategories() para que retornen la entidad de relaciones que creaste.

@Transaction
@Query("SELECT id, name, is_favorite FROM shopping_list")
abstract LiveData<List<ShoppingListWithCollaborators>> getAll();

@Transaction
@Query("SELECT id, name, is_favorite FROM shopping_list WHERE category IN(:categories)")
abstract LiveData<List<ShoppingListWithCollaborators>> getShoppingListsByCategories(List<String> categories);

Cuando termines estas acciones deberás actualizar el Adaptador, ViewModel y Repositorio para que acepten el nuevo tipo en sus métodos.

4. Actualizar Layout Del Item

Para que el layout quede igual al prototipo es necesario que agregues dos TextViews a shopping_list_item.xml.

Diseño UI para lista de compras con colaboradores

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" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

5. Bindear Colaboradores En Adaptador

Ahora muestra los nombres de los colaboradores en un String separado por comas en el ViewHolder del adaptador.

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

    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);

        // 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.createdDate);
        mCollaboratorsText.setText(TextUtils.join(",", item.collaboratorNames));
    }
}

6. Insertar Colaboradores

Modifica el método del DAO de la inserción de listas de compras para que acepte una lista colaboradores.

@Transaction
public void insertWithInfoAndCollaborators(ShoppingListInsert shoppingList,
                                           Info info, List<Collaborator> collaborators) {
    insertShoppingList(shoppingList);
    insertInfo(info);
    insertAllCollaborators(collaborators);
}

@Transaction
public void insertAllWithInfosAndCollaborators(List<ShoppingListInsert> shoppingLists,
                                               List<Info> infos,
                                               List<Collaborator> collaborators) {
    insertAll(shoppingLists);
    insertAllInfos(infos);
    insertAllCollaborators(collaborators);
}

@Insert(onConflict = OnConflictStrategy.IGNORE)
protected abstract void insertAllCollaborators(List<Collaborator> collaborators);

Seguido, abre ShoppingListDatabase e inserta cinco colaboradores en la escucha de apertura de la base de datos.

// Prepoblar base de datos con callback
private static final RoomDatabase.Callback mRoomCallback = new Callback() {
    @Override
    public void onOpen(@NonNull SupportSQLiteDatabase db) {
        super.onCreate(db);

        dbExecutor.execute(this::prepopulate);
    }

    public void prepopulate() {
        ShoppingListDao dao = INSTANCE.shoppingListDao();

        List<ShoppingListInsert> lists = new ArrayList<>();
        List<Info> infos = new ArrayList<>();
        List<Collaborator> collaborators = 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);

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

        dao.insertAllWithInfosAndCollaborators(lists, infos, collaborators);
    }
};

Ya finalizando, ejecuta el aplicativo. Deberás ver la siguiente imagen:

Relaciones Uno A Muchos Con Room

Siguiente tutorial: Relaciones Muchos A Muchos Con 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!