En este tutorial veremos cómo implementar relaciones uno a uno con Room entre nuestras tablas.
Para ello aprenderemos que son los objetos embebidos (@Embedded
), como establecer las anotaciones @Relation
para la relación 1:1 y establecer claves foráneas.
Que Son Embedded Objects
La anotación @Embedded
marca un campo de una @Entity
o POJO como un objeto anidado.
Es decir, un objeto que es interpretado como parte de otro.
Si el campo está en una entidad, entonces los atributos del objeto anidado serán tomados como columnas de la tabla.
Ejemplo:
Si removemos las fechas de creación y última actualización de nuestra entidad ShoppingList
en otra clase:
public class Life {
@ColumnInfo(name = "created_date", defaultValue = "CURRENT_TIMESTAMP")
public String createdDate;
@ColumnInfo(name = "last_updated", defaultValue = "CURRENT_TIMESTAMP")
public String lastUpdated;
}
Podemos decirle a Room que aún son parte de la tabla shopping_list
usando @Embedded
en un campo del tipo que contiene ambos atributos:
@Entity(tableName = "shopping_list")
public class ShoppingList {
@Embedded
public Life life;
}
Relaciones Uno A Uno
Una relación uno a uno (1:1) es: una relación donde cada instancia de la entidad padre está asociada con una y solo una instancia de la entidad hija.
Partamos de un ejemplo que aplicaremos en nuestra App de listas de compras.
Decidiremos que la tabla shoping_list
ahora tendrá una relación 1:1 con una nueva tabla llamada info
.
¿Cómo implementar esta relación en Room?
Seguiremos los siguientes pasos:
Paso 1: Añadir campo de referencia a la clave primaria de la entidad hija. Al crear la entidad Info
pondremos el Id de ShoppingList
:
@Entity(tableName = "info")
public class Info {
@NonNull
@PrimaryKey
public String id;
@NonNull
@ColumnInfo(name = "shopping_list_id")
public String shoppingListId;
@ColumnInfo(name = "created_date", defaultValue = "CURRENT_TIMESTAMP")
public String createdDate;
@ColumnInfo(name = "last_updated", defaultValue = "CURRENT_TIMESTAMP")
public String lastUpdated;
public Info(@NonNull String id, @NonNull String shoppingListId,
String createdDate, String lastUpdated) {
this.id = id;
this.shoppingListId = shoppingListId;
this.createdDate = createdDate;
this.lastUpdated = lastUpdated;
}
}
Paso 2: Crear una clase que contenga una instancia embebida de la clase padre (o POJO parcial de la misma) y otra de la hija anotada con @Relation
.
public class ShoppingListAndInfo {
@Embedded
public ShoppingListForList shoppingList;
@Relation(
parentColumn = "id",
entityColumn = "shopping_list_id"
)
public Info info;
}
Donde, la propiedad parentColumn
es para el nombre de la clave primaria del padre y entityColumn
para el nombre de la clave foránea en la hija.
Paso 3: Añadir método de retorno en el DAO en una transacción ya que son dos consultas (@Transaction
):
@Dao
public abstract class ShoppingListDao {
@Transaction
@Query("SELECT id, name, is_favorite FROM shopping_list")
abstract LiveData<List<ShoppingListAndInfo>> getAll();
@Transaction
@Query("SELECT id, name, is_favorite FROM shopping_list WHERE category IN(:categories)")
abstract LiveData<List<ShoppingListAndInfo>> getShoppingListsByCategories(List<String> categories);
}
Como ves, estamos pasando de interfaz a clase abstracta en el DAO, ya que operar relaciones requiere más de un método en la mayoría de casos.
Declarar Llaves Foráneas
Para especificar una restricción foránea entre dos entidades de Room usaremos la anotación @ForeignKey
.
Ubicaremos esta anotación en la entidad hija como parte de la propiedad foreignKeys
.
Ejemplo:
Agregar una referencia foránea para shoppingListId
en Info
:
@Entity(tableName = "info",
foreignKeys = @ForeignKey(
entity = ShoppingList.class,
parentColumns = "id",
childColumns = "shopping_list_id")
)
public class Info {
@NonNull
@ColumnInfo(name = "shopping_list_id")
public String shoppingListId;
}
Ejemplo De Relaciones Uno A Uno Con Room
En este corto ejemplo mostraremos los resultados del DAO que mapea la relación 1:1 entre las listas de compras y su información temporal.
Para ello aumentaremos el contenido del layout de cada ítem en la actividad principal, agregando un texto para la fecha de creación y mejorando el diseño con CardViews:
Puedes descargar el código final con las modificaciones desde el siguiente enlace:
Habrán algunas modificaciones que omitiré, por lo que te vendría muy bien decargarlo si no puedes realizarlas por ti mism@.
Veamos los pasos generales para llegar al resultado deseado:
1. Agregar Entidad A La Base De Datos
Añade a la propiedad entities
la nueva entidad que hemos declarado para los metadatos de la listas de compras:
@Database(entities = {ShoppingList.class, Info.class}, version = 4, exportSchema = false)
public abstract class ShoppingListDatabase extends RoomDatabase {
}
Ahí mismo sube el número de versión a 4
para actualizar el esquema.
2. Insertar Lista De Compras Junto A Info
Al mover las columnas de la tabla shopping_list
es necesario insertar la lista junto al registro de la info en un método del DAO de la siguiente forma:
@Transaction
public void insertWithInfo(ShoppingListInsert shoppingList, Info info) {
insertShoppingList(shoppingList);
insertInfo(info);
}
@Transaction
public void insertAllWithInfos(List<ShoppingListInsert> shoppingLists, List<Info> infos) {
insertAll(shoppingLists);
insertAllInfos(infos);
}
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = ShoppingList.class)
abstract void insertShoppingList(ShoppingListInsert shoppingList);
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = ShoppingList.class)
abstract void insertAll(List<ShoppingListInsert> lists);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract void insertInfo(Info info);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract void insertAllInfos(List<Info> infos);
En este caso insertWithInfo()
e insertAllWithInfos()
insertan una lista de compras junto a su información de fechas
Al ser dos operaciones de inserción anotamos el método con @Transaction
.
Esto modificaría la prepoblación que hacemos en 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(() -> {
ShoppingListDao dao = INSTANCE.shoppingListDao();
List<ShoppingListInsert> lists = new ArrayList<>();
List<Info> infos = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// Crear lista de compras
ShoppingListInsert shoppingList = new ShoppingListInsert(
String.valueOf((i + 1)),
"Lista " + (i + 1)
);
// Crear info
String date = Utils.getCurrentDate();
Info info = new Info(String.valueOf((i+1)),
shoppingList.id, date, date);
lists.add(shoppingList);
infos.add(info);
}
dao.insertAllWithInfos(lists, infos);
});
}
};
Y se propaga desde la actividad principal hacia el repositorio al momento de agregar una nueva lista de compras:
public void insert(ShoppingListInsert shoppingList) {
String date = Utils.getCurrentDate();
Info info = new Info(UUID.randomUUID().toString(), shoppingList.id, date, date);
mRepository.insert(shoppingList, info);
}
3. Crear Transacción Para Marcar Favorito
Otro cambio en el DAO que tenemos que hacer es cambiar el método markFavorite()
por dos actualizaciones, ya que intercambiamos el valor de shopping_list.is_favorite
y el de info.last_updated
al presionar el botón de favorito.
@Transaction
public void markFavorite(String id) {
updateShoppingListFavorite(id);
updateInfoLastUpdated(id);
}
@Query("UPDATE shopping_list SET is_favorite= NOT is_favorite WHERE id = :id")
protected abstract void updateShoppingListFavorite(String id);
@Query("UPDATE info SET last_updated = CURRENT_TIMESTAMP WHERE shopping_list_id=:shoppingListId")
protected abstract void updateInfoLastUpdated(String shoppingListId);
4. Actualizar Diseño De Layout
Para llegar al diseño propuesto en el prototipo debemos agregar como tag raíz un CardView
que contenga al ConstraintLayout
actual. Y dos TextViews para la etiqueta de la fecha de creación y su valor.
La siguiente es la definición XML con los cambios:
<?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="?listPreferredItemHeightLarge"
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"
tools:text="26/05/2020 01:12:54"
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="1.0" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
5. Usar Clase De Relación Uno A Uno
Reemplaza en el adaptador la lista de datos ShoppingListForList
por ShoppingListAndInfo
. Luego bindea la fecha de creación en el ViewHolder
.
public class ShoppingListViewHolder extends RecyclerView.ViewHolder {
private final TextView mNameText;
private final CheckBox mFavoriteButton;
private final ImageView mDeleteButton;
private TextView mCreatedDateText;
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);
// Setear eventos
mFavoriteButton.setOnClickListener(this::manageEvents);
mDeleteButton.setOnClickListener(this::manageEvents);
itemView.setOnClickListener(this::manageEvents);
}
private void manageEvents(View view) {
if (mItemListener != null) {
ShoppingListAndInfo 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(ShoppingListAndInfo item) {
mNameText.setText(item.shoppingList.name);
mFavoriteButton.setChecked(item.shoppingList.favorite);
mCreatedDateText.setText(item.info.createdDate);
}
}
6. Actualiza El ViewModel Y Repositorio
Propaga el cambio en el consturctor de ShoppingListViewModel
para que el repositorio use como tipo de retorno la nueva entidad de relación en los métodos getAll()
y getShoppingListsWithCategories()
.
public LiveData<List<ShoppingListAndInfo>> getShoppingLists() {
return mShoppingListDao.getAll();
}
public LiveData<List<ShoppingListAndInfo>> getShoppingListsWithCategories(List<String> categories) {
return mShoppingListDao.getShoppingListsByCategories(categories);
}
Una vez realizados los cambios, ejecuta el proyecto y verás los registros finales de la relación uno a uno en la interfaz:
Siguiente tutorial: Relaciones Uno 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!