如何在Room中过滤嵌套关系?

53
让我们看一个例子:我有一个表单,它有几个部分,每个部分都有问题。在旁边,我有与问题对应的答案,并且当查询时,我想过滤掉另一列:

数据库模式

因此,我有以下实体:

@Entity(tableName = "sections")
public class Section {
    @PrimaryKey
    public long id;
    public String title;
}
@Entity(tableName = "questions")
public class Question {
    @PrimaryKey
    public long id;
    public String title;
    public long sectionId;
}
@Entity(tableName = "answers")
public class Answer {
    @PrimaryKey
    public long id;
    public long questionId;
    public int otherColumn;
}

在DAO部分,我希望能够检索它们所有的内容。
以下是我想要通过此查询填充的POJO:
class SectionWithQuestions {
    @Embedded
    public Section section;

    @Relation(parentColumn = "id", entityColumn = "sectionId", entity = Question.class)
    public List<QuestionWithAnswer> questions;

    public static class QuestionWithAnswer {
        @Embedded
        public Question question;

        @Relation(parentColumn = "id", entityColumn = "questionId", entity = Answer.class)
        List<Answer> answers;
    }
}

在另一个应用程序中,查询将会是:
SELECT s.*, q.*, a.*
FROM sections s
LEFT JOIN questions q ON q.sectionId = s.id
LEFT JOIN answers a ON a.questionId = q.id
WHERE s.id = :sectionId and a.otherColumn = :otherColumn

然而在 Room 中,我发现如果你想要一个对象及其关系(例如示例中的用户和宠物),你只需选择该对象,关系将在第二个查询中查询。代码如下:

@Query("SELECT * FROM sections WHERE id = :sectionId")

然后在生成的代码中,会有以下伪代码:
sql = "SELECT * FROM sections WHERE id = :sectionId" // what's inside @Query
cursor = query(sql)
int indexColumn1 = cursor.getColumnIndex(col1)
int indexColumn2
... etc
while (cursor.moveToNext) {
    masterObject = new object()
    masterObject.property1 = cursor.get(indexColumn1)
    ... etc

    __fetchRelationshipXXXAsYYY(masterObject.relations) // fetch the child objects
}

这个 __fetch XXX as YYY 方法如下:

sql = "SELECT field1, field2, ... FROM a WHERE foreignId IN (...)"
similar algo as previously: fetch column indices, and loop through the cursor

基本上,它创建了两个查询:一个用于主对象,另一个用于关系。第二个查询是自动创建的,我们无法控制它。
要回到我的问题,在我想要关系但也要过滤子列的情况下,我陷入了困境:
在第一个查询中,我不能引用otherColumn列,因为它不存在;在@Relation中也不行,因为这个注释的唯一属性是连接列和实体定义。
Room是否支持这种操作,还是我必须自己创建子查询?
额外问题:为什么他们不在单个查询中连接表格而是创建两个查询?这是出于性能考虑吗?
编辑以澄清我的期望:
@Query("SELECT s.*, q.*, a.* " +
       "FROM sections s " +
       "LEFT JOIN questions q ON q.sectionId = s.id " +
       "LEFT JOIN answers a ON a.questionId = q.id " +
       "WHERE s.id = :sectionId and a.otherColumn = :additionalIntegerFilter")
SectionWithQuestionsAndAnswers fetchFullSectionData(long sectionId);

static class SectionWithQuestionsAndAnswers {
    @Embedded Section section;
    @Relation(parentColumn = "id", entityColumn = "sectionId", entity = Question.class)
    List<QuestionWithAnswers> questions;
}
static class QuestionWithAnswers {
    @Embedded Question question;
    @Relation(parentColumn = "id", entityColumn = "questionId", entity = Answer.class)
    Answer answer; // I already know that @Relation expects List<> or Set<> which is
                   // not useful if I know I have zero or one relation (ensured
                   // through unique keys)
}

这是我想象中由Room生成的伪代码:

function fetchFullSectionData(long sectionId, long additionalIntegerFilter) {
    query = prepare(sql); // from @Query
    query.bindLong("sectionId", sectionId);
    query.bindLong("additionalIntegerFilter", additionalIntegerFilter);
    cursor = query.execute();
    Section section = null;
    long prevQuestionId = 0;
    Question question = null;
    while (cursor.hasNext()) {
        if (section == null) {
            section = new Section();
            section.questions = new ArrayList<>();
            section.field1 = cursor.get(col1); // etc for all fields
        }
        if (prevQuestionId != cursor.get(questionIdColId)) {
            if (question != null) {
                section.questions.add(question);
            }
            question = new Question();
            question.fiedl1 = cursor.get(col1); // etc for all fields
            prevQuestionId = question.id;
        }
        if (cursor.get(answerIdColId) != null) { // has answer
            Answer answer = new Answer();
            answer.field1 = cursor.get(col1); // etc for all fields
            question.answer = answer;
        }
    }
    if (section !=null && question != null) {
        section.questions.add(question);
    }
    return section;
}

这是一个查询语句,能够获取到我所有的对象。


在DAO部分,我想要检索它们所有。-- 不根据您问题的其余部分。您只想检索具有特定“otherColumn”值的“Answer”关联的子集。 “在Room中是否可能?” -- 我认为不可能在单个请求中完成。您需要适当地请求适当的DAO并将结果拼接在一起。 “为什么他们不在单个查询中连接表,而是创建2个查询?这是出于性能原因吗?” -- 我猜这是出于“天哪,这只是1.0.0版本,请给我一个机会”的原因。 :-) - CommonsWare
我的意思是在另一个应用程序中(纯Java,PHP或其他任何语言),我将使用具有两个联接的请求,该请求将返回一个部分、它所有的问题和它们可能的答案。所以我将获得一个部分,许多问题,每个问题可能有一个答案。那是一个查询,并且从单个查询中得到许多对象。看来Room无法处理所有对象的这个查询。似乎我必须使用一个查询来选择一个部分及其所有的问题,然后循环问题并查询答案。这是DAO中的2个步骤,2个查询+每个问题1个查询。 - Benoit Duffez
似乎Room无法处理我所有对象的一个查询。我还没有查看过像你这样的三级层次结构生成的代码,但你所描述的并不让我感到意外。你可以提交一个功能请求;我猜测这将是1.0.0之后的一个举措。 - CommonsWare
实际上,关系的第二级并不是我的问题,我设计问题很糟糕。相关的是连接表上的过滤器,因为连接表实际上不是一个JOIN,而且我不能在关系中添加除关系ID之外的过滤器。我想过滤answer.otherColumn。也许这是一个功能请求。我已经编辑了我的问题,并提供了更多关于我的期望的细节。 - Benoit Duffez
为了更好地理解,一方面我有“表单”(一个表单有多个部分,每个部分有多个问题),另一方面我有“回复”(一个回复对应一个表单,有多个答案)。因此,每个答案对象都有一个外键指向它所回答的问题对象,以及另一个外键指向它所属的回复对象。如果这不清楚的话,我可以将这些表格添加到我的问题模式图中。 - Benoit Duffez
2
回复:功能请求:[已发布#65509934](https://issuetracker.google.com/issues/65509934) - Benoit Duffez
3个回答

0

我发现房间关系很难处理,不太灵活,而且大部分工作都是在底层完成的,很难确切知道如何操作。

在我的项目中,大部分时间我只创建演示对象 - 专门用于某些 UI 演示的对象,可以填充自定义选择。

这样我就可以更好地控制我想从数据库中获取什么(即我真正需要的内容),并将其填充到该自定义演示对象中。


0

我只是复制了我发布的功能请求的信息(请参见我的问题评论):

嗨 - 我们最近发布了一个新功能,其中可以使用Multimap返回类型定义关系查询方法。 有了这个新功能,您应该能够实现本主题中讨论的结果。 有关此新功能的更多信息,您可以查看以下资源:

我知道仅包含链接的答案不是很好,但我没有机会测试这个。如果有更好的答案,我会接受它。


0
我找到了一个更好的解决方案。不需要给所有列取别名,你可以使用@RawQuery注释。
首先,使用表名或其别名为嵌入式表注释添加前缀,例如@Embedded(prefix = "P.")@Embedded(prefix = "Post.")
public class UserPost {

    @Embedded
    private User user;

    @Embedded(prefix = "P.")
    private Post post;

}

然后在你的Dao中,创建一个函数来运行原始查询,再创建另一个函数来运行原始查询:

@Dao
public interface UserDao {

    String USER_POST_QUERY = "SELECT U.*, P.* FROM User as U " +
            "INNER JOIN Post as P ON U.id = P.userId " +
            "WHERE P.status = 1";

    @RawQuery
    LiveData<List<UserPost>> rawQuery(SimpleSQLiteQuery query);

    default LiveData<List<UserPost>> getAlertViolationsAsync() {
        return rawQuery(new SimpleSQLiteQuery(USER_POST_QUERY));
    }
}

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