Android Room:使用Room插入关联实体

113

我在Room中使用Relation添加了一个一对多的关系。我参考了这篇帖子编写了关于Room中关系的以下代码。

这篇帖子告诉了如何从数据库读取值,但是将实体存储到数据库中导致userId为空,这意味着两个表之间没有关系。

我不确定在具有userId值的情况下如何将UserList of Pet插入到数据库中。

1) User实体:

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

2) 宠物实体:

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

现在我们使用以下 DAO 来从数据库中获取记录:

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

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

编辑

我在问题追踪器上创建了这个问题https://issuetracker.google.com/issues/62848977。希望他们会对此采取一些行动。


所以他们说“关系”是2.2更新的重点。当前版本是2018年12月4日发布的“Room 2.1.0-alpha03”。 - Lech Osiński
是的,只需在问题跟踪器中阅读他们的评论。这需要时间,与此同时,您可以使用解决方法。 - Akshay Chordiya
1
他们已经将这个问题降低了优先级(2021年)。 - Chandrahas Aroori
6个回答

47

您可以通过将Dao从接口更改为抽象类来实现此操作。

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

你还可以深入探讨,通过使User对象拥有一个@Ignored List<Pet> pets

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

    @Ignored
    public List<Pet> pets
}

然后,Dao可以将UserWithPets映射到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;
}

这将留下完整的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();
}

您可能希望在PetDAO中添加insertAll(List<Pet>)insertPetsForUser(User,List<Pet>)方法...如何划分DAO取决于您! :)

无论如何,这只是另一个选项。将DAO封装在DataSource对象中也可以工作。


把便捷方法放在抽象类中而不是接口中是一个很好的想法。 - Akshay Chordiya
13
如果我们将与宠物相关的方法移动到PetDao中,那么我们该如何在UserDao中引用PetDao的方法? - sbearben
6
谢谢您的问题。但是在insertPetsForUser(User user, List<Pet> pets)方法中,pet.setUserId(user.getId())如何设置userId?假设您从API获取用户对象,并且此时它没有id(主键),因为它尚未保存到RoomDb,因此调用user.getId()只会产生默认值0,对于每个用户,因为它尚未保存。那么你如何处理这样的情况,因为user.getId()始终为每个用户返回0。谢谢 - Ikhiloya Imokhai
自动生成ID的问题有解决方案了吗? - razz
@IkhiloyaImokhai 我认为 insertPetsForUser(User user, List<Pet> pets) 方法忘记先调用 insertUser(User user) 以获取 userId。为了使其正常工作,inserUser(User user) 方法应该返回一个 long 类型的 userId,即方法签名必须是 public abstract long insertUser(User user) - Xam

46

在 Room Library 进行任何更新之前,没有本地解决方案,但可以通过一个技巧来实现。请参考下面的步骤。

  1. 创建一个带有宠物的用户(忽略宠物)。添加 getter 和 setter。请注意,我们必须手动设置我们的 ID,并且无法使用 autogenerate

    @Entity
    public class User {
          @PrimaryKey
          public int id; 
    
          @Ignore
          private List<Pet> petList;
    }
    
  2. 创建宠物。

  3. @Entity 
    public class Pet 
    {
        @PrimaryKey
        public int id;     
        public int userId; 
        public String name;
    }
    
  4. UserDao 应该是一个抽象类而不是一个接口。然后在你的 UserDao 中最终实现它。

  5. @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;
    }
    

    您可以通过这种方式解决问题,而无需创建UserWithPets POJO对象。


2
我喜欢这个方案,因为它避免了使用UserWithPets POJO。通过使用默认方法,DAO也可以是一个接口。我唯一看到的缺点是insertUser()和insertPetList()是公共方法,但不应该被客户端使用。我在这些方法的名称前面加了下划线,以表明它们不应该被使用,如上所示。 - guglhupf
1
有人能展示一下如何在Activity中正确实现这个吗?我现在不知道如何正确生成这些Ids。 - Philipp
@Philipp,我正在处理如何生成ID的问题,你找到解决方案了吗? - sochas
1
谢谢你的回复@Philipp,我也是用相同的方式做的 :) - sochas
3
谢谢@Philipp,我很喜欢你的回答,因为它很灵活!对于自动生成的id,在我的情况下,我首先调用insertUser()获取自动生成的userId,然后将该userId分配给Pet类中的userId字段,然后循环调用insertPet() - Robert
显示剩余3条评论

13

由于 Room 不管理实体之间的关系,因此您需要自己设置每个宠物的 userId 并保存它们。只要一次没有太多宠物,我建议使用 insertAll 方法来使代码更简洁。

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

我认为目前没有更好的方法。

为了使处理更加容易,我会在DAO层上方使用一个抽象层:

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

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

    petDao.insertAll(pets);
}

我尝试使用“Stream”来完成相同的操作。只是希望有更好的方法来处理它。 - Akshay Chordiya
我认为没有更好的方法,因为文档中明确指出他们有意将引用从库中删除:https://developer.android.com/topic/libraries/architecture/room.html#no-object-references - tknell
我在问题跟踪器上创建了这个问题https://issuetracker.google.com/issues/62848977。希望他们能对此采取一些行动。 - Akshay Chordiya
1
虽然不是一个原生的解决方案,但你并不一定需要使用接口来实现DAO(数据访问对象),你也可以使用抽象类。这意味着如果你不想要一个包装类,便可以将便利方法直接放在DAO内部。请参考我的答案获取更多信息。 - Kieran Macdonald-Hall

7

目前还没有本地解决方案。我在Google的问题跟踪器上创建了https://issuetracker.google.com/issues/62848977,架构组件团队表示他们将在Room库的v1.0版本中或之后添加本地解决方案。

临时解决方案:

同时,您可以使用tknell提到的解决方案。

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

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

    petDao.insertAll(pets);
}

看了其他的解决方案,我发现使用抽象类而不是接口来创建dao并添加相似的具体方法是一种可行的方式。但是,我仍然不明白如何在这些方法中获取子dao实例?例如,你是如何在userDao中获取petDao实例的? - Purush Pawar

6

我成功地使用了一个相对简单的解决方法来正确地插入它。这是我的实体:

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

我正在使用autoGenerate自动生成自增值(long类型被用于特定目的)。以下是我的解决方案:

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

我在链接实体时遇到了问题,自动生成的结果一直是"recipeId=0"。先插入食谱实体就解决了我的问题。


哇,这个答案很好用。使用Long生成可插入的id返回函数是如何实现的?这是一种黑客技巧,还是一些官方文档中没有的函数? - Jeevan MB
这是什么意思:**getIngredients()**, **recipeWithIngredients.getRecipe()**。 - user17115124
这些是Java中的getter方法。你来自Kotlin背景还是新手? - ikmazameti

5

现在的v2.1.0版本中,Room似乎不适合具有嵌套关系的模型。需要大量的样板代码来维护它们。例如:手动插入列表、创建和映射本地ID等。

这些关系映射操作可以通过Requery轻松完成:https://github.com/requery/requery此外,它没有插入枚举类型的问题,并且具有其他复杂类型(如URI)的转换器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接