113

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.

Akshay Chordiya
  • 4,761
  • 3
  • 40
  • 52

6 Answers6

47

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.

  • Great idea to put the convenience method in abstract class instead of interface. – Akshay Chordiya Oct 31 '17 at 17:27
  • 13
    if we were to move the Pet related methods into the PetDao, how would we reference the PetDao methods within the UserDao? – sbearben Oct 31 '18 at 20:50
  • 6
    thanks for your answer. But how does pet.setUserId(user.getId()) in insertPetsForUser(User user, List pets) method sets the userId? let's assume your're getting the user object from an API and at this point it has no id(primaryKey) since it has not been saved to RoomDb, so calling user.getId() will only yield a default value of 0, for each user as it is yet to be saved. How do you handle such because user.getId() always return 0 for each user. Thanks – Ikhiloya Imokhai Mar 11 '19 at 10:40
  • Any solution for auto-generated id's problem yet? – razz Aug 29 '21 at 00:53
  • @IkhiloyaImokhai I think the method `insertPetsForUser(User user, List pets)` forgot to call `insertUser(User user)` first in order to get the userId. For this to work the method `inserUser(User user)` should return a long, which will be the userId, i.e, the method signature must be `public abstract long insertUser(User user)` – Xam Mar 17 '22 at 20:21
46

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.

Philipp
  • 335
  • 1
  • 6
  • 17
Dipendra Sharma
  • 2,404
  • 2
  • 18
  • 35
  • 2
    I like this one, because it avoids the UserWithPets POJO. By using default methods the DAO can be an interface as well. Only downside I see is that insertUser() and insertPetList() are public methods, but should not be used by a client. I put an underscore before the name of these methods to show that they should not be used, as shown above. – guglhupf Nov 23 '17 at 20:50
  • 1
    Can someone show how to properly implement this in an Activity? Right know I don't know how to generate the Ids properly. – Philipp Nov 26 '19 at 14:52
  • @Philipp I'm dealing with how to generate the ids, did you find a solution? – sochas Dec 04 '19 at 10:26
  • @sochas Not really, I generated them in my activity because I did not found a better way. – Philipp Dec 05 '19 at 12:39
  • 1
    Thaks for your reply @Philipp, I did it on the same way :) – sochas Dec 05 '19 at 12:51
  • 3
    Thanks @Philipp I like your answer for its flexibility! For auto generating ids, in my case I call `insertUser()` first to get auto-generated `userId`, then assign that `userId` to userId field in Pet class, then looping to `insertPet()`. – Robert Jan 02 '20 at 02:17
  • How can you call `insertPetList(pets);` before `insertUser(user)`? Wouldn't the pet record reference a non-existent user record before `insertUser(user)`? – ericn Apr 29 '21 at 19:05
  • You can add "long" as the returned type of the @Insert operation, it will return the Id of the inserted Item. – Martin Olariaga Jan 14 '23 at 23:03
13

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);
}
tknell
  • 9,007
  • 3
  • 24
  • 28
  • I tried the same using `Stream`. I just hope there is a better way to do it. – Akshay Chordiya Jun 21 '17 at 05:37
  • I don't think there is a better way, as the documentation says that they intentionally left references out of the library: https://developer.android.com/topic/libraries/architecture/room.html#no-object-references – tknell Jun 21 '17 at 05:54
  • I have created this issue https://issuetracker.google.com/issues/62848977 on the issue tracker. Hopefully they will do something regarding it. – Akshay Chordiya Jun 21 '17 at 08:31
  • 1
    Not really a native solution, but you don't have to use interfaces for DAOs, you can use abstract classes too... meaning the convenience methods can sit within DAO itself if you don't want to have a wrapper class. See my answer for more info... – Kieran Macdonald-Hall Oct 31 '17 at 16:17
7

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);
}
Akshay Chordiya
  • 4,761
  • 3
  • 40
  • 52
  • Looking at other solutions, I am seeing way to go about this is using an abstract class instead of an interface while creating the dao and adding similar concrete methods part of those abstract classes. But, I still can't understand how to get child dao instance inside these methods? eg. how are you able to get petDao instance inside userDao? – Purush Pawar Nov 08 '20 at 15:28
6

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.

  • Wow. This answer works. How does using Long generate a id returnable insert function? Is it some sort of hack, or some room function which is not there in official docs? – Jeevan MB Oct 23 '21 at 16:58
  • what is these: **getIngredients()** , **recipeWithIngredients.getRecipe()** –  Jan 23 '22 at 13:54
  • These are getters in Java. Are you from Kotlin background or a newbie? – ikmazameti Dec 17 '22 at 12:36
5

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.

MainActivity
  • 1,650
  • 2
  • 12
  • 16