Android Room: inserte entidades de relación usando Room

88

He agregado una relación a muchas en Room usando Relation . Me referí a esta publicación para escribir el siguiente código de relación en Room.

La publicación dice cómo leer los valores de la base de datos, pero almacenar las entidades en la base de datos resultó en userIdestar vacío, lo que significa que no hay relación entre las 2 tablas.

No estoy seguro de lo que es la forma ideal para insertuna Usery List of Peten la base de datos mientras que tener userIdvalor.

1) Entidad de usuario:

@Entity
public class User {
    @PrimaryKey
    public int id; // User id
}

2) Entidad de mascota:

@Entity
public class Pet {
    @PrimaryKey
    public int id;     // Pet id
    public int userId; // User id
    public String name;
}

3) UserWithPets POJO:

// Note: No annotation required at this class definition.
public class UserWithPets {
   @Embedded
   public User user;

   @Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class)
   public List<Pet> pets;
}

Ahora, para obtener los registros de la base de datos, usamos lo siguiente DAO:

@Dao
public interface UserDao {
    @Insert
    fun insertUser(user: User)

    @Query("SELECT * FROM User")
    public List<UserWithPets> loadUsersWithPets();
}

EDITAR

He creado este problema https://issuetracker.google.com/issues/62848977 en el rastreador de problemas. Con suerte, harán algo al respecto.

Akshay Chordiya
fuente
Entonces dicen que "relaciones" es la prioridad en la actualización 2.2. La versión actual es "Room 2.1.0-alpha03" del 4 de diciembre de 2018.
Lech Osiński
Sí, solo lea su comentario en el rastreador de problemas. Tomará tiempo, mientras tanto, puede utilizar las soluciones alternativas
Akshay Chordiya

Respuestas:

43

Puede hacer esto cambiando su Dao de una interfaz a una clase abstracta.

@Dao
public abstract class UserDao {

    public void insertPetsForUser(User user, List<Pet> pets){

        for(Pet pet : pets){
            pet.setUserId(user.getId());
        }

        _insertAll(pets);
    }

    @Insert
    abstract void _insertAll(List<Pet> pets);  //this could go in a PetDao instead...

    @Insert
    public abstract void insertUser(User user);

    @Query("SELECT * FROM User")
    abstract List<UserWithPets> loadUsersWithPets();
}

También puede ir más allá haciendo que un Userobjeto tenga una@Ignored List<Pet> pets

@Entity
public class User {
    @PrimaryKey
    public int id; // User id

    @Ignored
    public List<Pet> pets
}

y luego el Dao puede mapear UserWithPetsal Usuario:

public List<User> getUsers() {
    List<UserWithPets> usersWithPets = loadUserWithPets();
    List<User> users = new ArrayList<User>(usersWithPets.size())
    for(UserWithPets userWithPets: usersWithPets) {
        userWithPets.user.pets = userWithPets.pets;
        users.add(userWithPets.user);
    }
    return users;
}

Esto te deja con el Dao completo:

@Dao
public abstract class UserDao {

    public void insertAll(List<User> users) {
        for(User user:users) {
           if(user.pets != null) {
               insertPetsForUser(user, user.pets);
           }
        }
        _insertAll(users);
    }

    private void insertPetsForUser(User user, List<Pet> pets){

        for(Pet pet : pets){
            pet.setUserId(user.getId());
        }

        _insertAll(pets);
    }

    public List<User> getUsersWithPetsEagerlyLoaded() {
        List<UserWithPets> usersWithPets = _loadUsersWithPets();
        List<User> users = new ArrayList<User>(usersWithPets.size())
        for(UserWithPets userWithPets: usersWithPets) {
            userWithPets.user.pets = userWithPets.pets;
            users.add(userWithPets.user);
        }
        return users;
    }


    //package private methods so that wrapper methods are used, Room allows for this, but not private methods, hence the underscores to put people off using them :)
    @Insert
    abstract void _insertAll(List<Pet> pets);

    @Insert
    abstract void _insertAll(List<User> users);

    @Query("SELECT * FROM User")
    abstract List<UserWithPets> _loadUsersWithPets();
}

Es posible que desee tener los métodos insertAll(List<Pet>)y insertPetsForUser(User, List<Pet>)en un PetDAO en su lugar ... ¡cómo particione sus DAO depende de usted! :)

De todos modos, es solo otra opción. Envolver sus DAO en objetos DataSource también funciona.

Kieran Macdonald-Hall
fuente
Gran idea para poner el método de conveniencia en una clase abstracta en lugar de una interfaz.
Akshay Chordiya
7
Si tuviéramos que mover los métodos relacionados con Pet al PetDao, ¿cómo haríamos referencia a los métodos PetDao dentro del UserDao?
sbearben
4
gracias por tu respuesta. Pero, ¿cómo pet.setUserId (user.getId ()) en el método insertPetsForUser (User user, List <Pet> pets) establece el userId? supongamos que está obteniendo el objeto de usuario de una API y en este punto no tiene id (primaryKey) ya que no se ha guardado en RoomDb, por lo que llamar a user.getId () solo producirá un valor predeterminado de 0, para cada usuario, ya que aún no se ha guardado. ¿Cómo se maneja? Porque user.getId () siempre devuelve 0 para cada usuario. Gracias
Ikhiloya Imokhai
38

No hay una solución nativa hasta cualquier actualización en la biblioteca de habitaciones, pero puede hacerlo con un truco. Encuentra abajo mencionado.

  1. Simplemente cree un usuario con mascotas (ignore las mascotas). Agrega getter y setter. Tenga en cuenta que tenemos que configurar nuestro Id manualmente más tarde y no podemos usar autogenerate.

    @Entity
    public class User {
          @PrimaryKey
          public int id; 
    
          @Ignore
          private List<Pet> petList;
    }
    
  2. Crea una mascota.

    @Entity 
    public class Pet 
    {
        @PrimaryKey
        public int id;     
        public int userId; 
        public String name;
    }
    
  3. UserDao debería ser una clase abstracta en lugar de una interfaz. Luego, finalmente en su UserDao.

    @Insert
    public abstract void insertUser(User user);
    
    @Insert
    public abstract void insertPetList(List<Pet> pets);
    
    @Query("SELECT * FROM User WHERE id =:id")
    public abstract User getUser(int id);
    
    @Query("SELECT * FROM Pet WHERE userId =:userId")
    public abstract List<Pet> getPetList(int userId);
    
    public void insertUserWithPet(User user) {
        List<Pet> pets = user.getPetList();
        for (int i = 0; i < pets.size(); i++) {
            pets.get(i).setUserId(user.getId());
        }
        insertPetList(pets);
        insertUser(user);
    }
    
    public User getUserWithPets(int id) {
        User user = getUser(id);
        List<Pet> pets = getPetList(id);
        user.setPetList(pets);
        return user;
    }
    

Su problema puede resolverse con esto sin crear UserWithPets POJO.

Dipendra Sharma
fuente
2
Me gusta este, porque evita el UserWithPets POJO. Al usar métodos predeterminados, el DAO también puede ser una interfaz. El único inconveniente que veo es que insertUser () e insertPetList () son métodos públicos, pero no deben ser utilizados por un cliente. Pongo un guión bajo antes del nombre de estos métodos para mostrar que no deben usarse, como se muestra arriba.
guglhupf
1
¿Alguien puede mostrar cómo implementar esto correctamente en una actividad? Bien sé que no sé cómo generar los ID correctamente.
Philipp
@Philipp Estoy tratando con cómo generar los identificadores, ¿encontraste una solución?
sochas
1
Gracias por tu respuesta @Philipp, lo hice de la misma manera :)
sochas
2
Gracias @Philipp ¡Me gusta tu respuesta por su flexibilidad! Para los identificadores de generación automática, en mi caso llamo insertUser()primero para generarlo automáticamente userId, luego lo asigno userIdal campo userId en la clase Pet y luego lo hago en bucle insertPet().
Robert
11

Como Room no gestiona las Relaciones de las entidades, debes configurarlo userIdtú mismo en cada mascota y guardarlas. Siempre que no haya demasiadas mascotas a la vez, usaría un insertAllmétodo para que sea breve.

@Dao
public interface PetDao {
    @Insert
    void insertAll(List<Pet> pets);
}

No creo que haya una mejor manera en este momento.

Para facilitar el manejo, usaría una abstracción en la capa sobre los DAO:

public void insertPetsForUser(User user, List<Pet> pets){

    for(Pet pet : pets){
        pet.setUserId(user.getId());
    }

    petDao.insertAll(pets);
}
tknell
fuente
Intenté lo mismo usando Stream. Solo espero que haya una mejor manera de hacerlo.
Akshay Chordiya
No creo que haya una mejor manera, ya que la documentación dice que intencionalmente dejaron referencias fuera de la biblioteca: developer.android.com/topic/libraries/architecture/…
tknell
He creado este problema issuetracker.google.com/issues/62848977 en el rastreador de problemas. Con suerte, harán algo al respecto.
Akshay Chordiya
1
No es realmente una solución nativa, pero no tiene que usar interfaces para DAO, también puede usar clases abstractas ... lo que significa que los métodos de conveniencia pueden ubicarse dentro de DAO si no desea tener una clase contenedora. Vea mi respuesta para obtener más información ...
Kieran Macdonald-Hall
6

Actualmente no existe una solución nativa para este problema. Creé este https://issuetracker.google.com/issues/62848977 en el rastreador de problemas de Google y el Equipo de Componentes de Arquitectura dijo que agregarán una solución nativa en o después de la v1.0 de la biblioteca de Room.

Solución temporal:

Mientras tanto, puede utilizar la solución mencionada por tknell .

public void insertPetsForUser(User user, List<Pet> pets){

    for(Pet pet : pets){
        pet.setUserId(user.getId());
    }

    petDao.insertAll(pets);
}
Akshay Chordiya
fuente
Mirando otras soluciones, veo que la manera de hacerlo es usar una clase abstracta en lugar de una interfaz mientras se crea el dao y se agregan métodos concretos similares que forman parte de esas clases abstractas. Pero, ¿todavía no puedo entender cómo obtener una instancia de dao infantil dentro de estos métodos? p.ej. ¿Cómo puede obtener una instancia de petDao dentro de userDao?
Purush Pawar
5

Ahora, en v2.1.0 Room parece no ser adecuado para modelos con relaciones anidadas. Necesitaba mucho código repetitivo para mantenerlos. Por ejemplo, inserción manual de listas, creación y mapeo de ID locales.

Estas operaciones de mapeo de relaciones las realiza Requery https://github.com/requery/requery. Además, no tiene problemas para insertar Enums y tiene algunos convertidores para otros tipos complejos como URI.

Actividad principal
fuente
2

Me las arreglé para insertarlo correctamente con una solución relativamente simple. Aquí están mis entidades:

   @Entity
public class Recipe {
    @PrimaryKey(autoGenerate = true)
    public long id;
    public String name;
    public String description;
    public String imageUrl;
    public int addedOn;
   }


  @Entity
public class Ingredient {
   @PrimaryKey(autoGenerate = true)
   public long id;
   public long recipeId; 
   public String name;
   public String quantity;
  }

public class RecipeWithIngredients {
   @Embedded
   public  Recipe recipe;
   @Relation(parentColumn = "id",entityColumn = "recipeId",entity = Ingredient.class)
   public List<Ingredient> ingredients;

Estoy usando autoGenerate para el valor de incremento automático (long se usa con propósitos). Aquí está mi solución:

  @Dao
public abstract class RecipeDao {

  public  void insert(RecipeWithIngredients recipeWithIngredients){
    long id=insertRecipe(recipeWithIngredients.getRecipe());
    recipeWithIngredients.getIngredients().forEach(i->i.setRecipeId(id));
    insertAll(recipeWithIngredients.getIngredients());
  }

public void delete(RecipeWithIngredients recipeWithIngredients){
    delete(recipeWithIngredients.getRecipe(),recipeWithIngredients.getIngredients());
  }

 @Insert
 abstract  void insertAll(List<Ingredient> ingredients);
 @Insert
 abstract long insertRecipe(Recipe recipe); //return type is the key here.

 @Transaction
 @Delete
 abstract void delete(Recipe recipe,List<Ingredient> ingredients);

 @Transaction
 @Query("SELECT * FROM Recipe")
 public abstract List<RecipeWithIngredients> loadAll();
 }

Tuve problemas para vincular las entidades, la generación automática produjo "RecetaId = 0" todo el tiempo. Al insertar la entidad de receta, primero me lo arregló.

stefan.georgiev.uzunov
fuente