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

Posted

技术标签:

【中文标题】Android Room:使用 Room 插入关系实体【英文标题】:Android Room: Insert relation entities using Room 【发布时间】:2017-11-23 19:55:24 【问题描述】:

我使用Relation 在 Room 中添加了一对多关系。 我参考this post为Room中的关系编写了以下代码。

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

我不确定在具有userId 值的同时将insertUserList of Pet 放入数据库的理想方法是什么。

1) 用户实体:

@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”。 是的,刚刚阅读了他们在问题跟踪器中的评论。这需要时间,同时您可以使用解决方法 他们降低了问题的优先级(2021 年)。 【参考方案1】:

您可以通过将 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&lt;Pet&gt; pets 来更进一步

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

    @Ignored
    public List<Pet> pets

然后 Dao 可以将UserWithPets 映射到用户:

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
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&lt;Pet&gt;)insertPetsForUser(User, List&lt;Pet&gt;) 方法……如何划分 DAO 取决于您! :)

无论如何,这只是另一种选择。将您的 DAO 包装在 DataSource 对象中也可以。

【讨论】:

将便捷方法放在抽象类而不是接口中的好主意。 如果我们将 Pet 相关的方法移动到 PetDao 中,我们将如何在 UserDao 中引用 PetDao 方法? 感谢您的回答。但是 insertPetsForUser(User user, List pets) 方法中的 pet.setUserId(user.getId()) 是如何设置 userId 的呢?假设您从 API 获取用户对象,此时它没有 id(primaryKey),因为它尚未保存到 RoomDb,因此调用 user.getId() 只会产生默认值 0,对于每个用户,因为它尚未保存。你如何处理这种情况,因为 user.getId() 总是为每个用户返回 0。谢谢 自动生成id的问题有什么解决办法吗?【参考方案2】:

在房间库中的任何更新之前都没有本地解决方案,但您可以通过一个技巧来做到这一点。找到下面提到的。

    只需创建一个带宠物的用户(忽略宠物)。添加 getter 和 setter。请注意,我们必须稍后手动设置我们的 ID,并且不能使用autogenerate

    @Entity
    public class User 
          @PrimaryKey
          public int id; 
    
          @Ignore
          private List<Pet> petList;
    
    

    创建宠物。

    @Entity 
    public class Pet 
    
        @PrimaryKey
        public int id;     
        public int userId; 
        public String name;
    
    

    UserDao 应该是一个抽象类而不是一个接口。然后最后在你的 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;
    
    

您的问题可以通过这个解决,而无需创建 UserWithPets POJO。

【讨论】:

我喜欢这个,因为它避免了 UserWithPets POJO。通过使用默认方法,DAO 也可以是一个接口。我看到的唯一缺点是 insertUser() 和 insertPetList() 是公共方法,但不应由客户端使用。我在这些方法的名称前加了一个下划线,表示不应该使用它们,如上图。 有人可以展示如何在活动中正确实现这一点吗?知道我不知道如何正确生成 Id。 @Philipp 我正在处理如何生成 id,您找到解决方案了吗? 感谢您的回复@Philipp,我也是这样做的 :) 谢谢@Philipp 我喜欢你的回答,因为它的灵活性!对于自动生成 id,在我的情况下,我首先调用 insertUser() 以获取自动生成的 userId,然后将 userId 分配给 Pet 类中的 userId 字段,然后循环到 insertPet()【参考方案3】:

由于 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做了同样的尝试。我只是希望有更好的方法来做到这一点。 我认为没有更好的方法,因为文档说他们故意将参考文献排除在库之外:developer.android.com/topic/libraries/architecture/… 我在问题跟踪器上创建了这个问题issuetracker.google.com/issues/62848977。希望他们会为此做点什么。 不是真正的原生解决方案,但您不必为 DAO 使用接口,您也可以使用抽象类...这意味着如果您不想要,便利方法可以位于 DAO 本身中有一个包装类。有关更多信息,请参阅我的答案...【参考方案4】:

目前没有针对此问题的本地解决方案。我在 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 实例?【参考方案5】:

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

这种关系映射操作由 Requery https://github.com/requery/requery 开箱即用地完成,此外它在插入 Enums 时没有问题,并且对于 URI 等其他复杂类型有一些转换器。

【讨论】:

【参考方案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 可返回的插入函数?是某种 hack,还是官方文档中没有的某种房间功能? 这些是什么:getIngredients()recipeWithIngredients.getRecipe()

以上是关于Android Room:使用 Room 插入关系实体的主要内容,如果未能解决你的问题,请参考以下文章

Android Room 插入所有问题

Android Room - 通过自动生成获取新插入行的 id

如何从 api 获取数据并将数据插入 Android Studio 中的 ROOM(数据库)?

在不触发发射的情况下插入 Android Room 表?

Android Room - 使用自动生成获取新插入行的 id - MVVM 版本

Android Room - 带有附加字段的多对多关系