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