Android Room: Insert relation entities using Room

AndroidAndroid RoomAndroid Architecture-Components

Android Problem Overview


I've added one to many relationship in Room using Relation. I referred to this post to write the following code for relation in Room.

The post tells how to read the values from the database but storing the entities into the database resulted in userId to be empty which means there is no relation between the 2 tables.

I'm not sure what is the ideal way to insert a User and List of Pet into the database while having userId value.

  1. User Entity:

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

  2. Pet Entity:

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

    }

Now to fetch the records from DB we use the following DAO:

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

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

EDIT

I have created this issue https://issuetracker.google.com/issues/62848977 on the issue tracker. Hopefully they will do something regarding it.

Android Solutions


Solution 1 - Android

You can do this by changing your Dao from an interface to an abstract class.

@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();
}

You can also go further by having a User object have an @Ignored List<Pet> pets

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

    @Ignored
    public List<Pet> pets
}

and then the Dao can map UserWithPets to User:

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

This leaves you with the full Dao:

@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();
}

You may want to have the insertAll(List<Pet>) and insertPetsForUser(User, List<Pet>) methods in a PetDAO instead... how you partition your DAOs is up to you! :)

Anyway, it's just another option. Wrapping your DAOs in DataSource objects also works.

Solution 2 - Android

There is no native solution till any update in Room Library but you can do this by a trick. Find below mentioned.

  1. Just Create a User with Pets (Ignore pets). Add getter and setter. Notice that we have to set our Id's manually later and can't use autogenerate.

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

     @Entity 
     public class Pet 
     {
        	@PrimaryKey
     	public int id;     
     	public int userId; 
     	public String name;
     }
    
  3. The UserDao should be an abstract class instead of an Interface. Then finally in your 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;
     }
    

Your problem can be solved by this without creating UserWithPets POJO.

Solution 3 - Android

As Room does not manage the Relations of the entities, you have to set the userId on each pet yourself and save them. As long as there are not too many pets at once, I'd use an insertAll method to keep it short.

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

I don't think there's any better way at the moment.

To make the handling easier, I'd use an abstraction in the layer above the DAOs:

public void insertPetsForUser(User user, List<Pet> pets){
    
    for(Pet pet : pets){
        pet.setUserId(user.getId());
    }

    petDao.insertAll(pets);
}

Solution 4 - Android

Currently there is no native solution to this problem. I have created this https://issuetracker.google.com/issues/62848977 on Google's issue tracker and the Architecture Components Team said they will adding a native solution in or after v1.0 of Room library.

Temporary Workaround:

Meanwhile you can use the solution mentioned by tknell.

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

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

    petDao.insertAll(pets);
}

Solution 5 - Android

Now at v2.1.0 Room seems to be not suitable for models with nested relations. It needed lots of boilerplate code to maintain them. E.g. manual insert of lists, creating and mapping local IDs.

This relations-mapping operations are done out of box by Requery https://github.com/requery/requery Additionaly it does not have issues with inserting Enums and have some converters for other complex types like URI.

Solution 6 - Android

I managed to insert it properly with a relatively simple workaround. Here are my entities:

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

I am using autoGenerate for auto-increment value(long is used with a purpoes). Here is my solution:

  @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();
 }

I had problem linking the entities, auto generate produced "recipeId=0" all the time. Inserting the recipe entity firstly fixed it for me.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAkshay ChordiyaView Question on Stackoverflow
Solution 1 - AndroidKieran Macdonald-HallView Answer on Stackoverflow
Solution 2 - AndroidDipendra SharmaView Answer on Stackoverflow
Solution 3 - AndroidtknellView Answer on Stackoverflow
Solution 4 - AndroidAkshay ChordiyaView Answer on Stackoverflow
Solution 5 - AndroidMainActivityView Answer on Stackoverflow
Solution 6 - Androidstefan.georgiev.uzunovView Answer on Stackoverflow