Icono del sitio Develou

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.

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

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.

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:


Siguiente tutorial: Relaciones Muchos A Muchos Con Room

Salir de la versión móvil