Запрос от одного до многих отношений Android Room

#android-sqlite #android-room #android-jetpack #android-database #android-room-relation

Вопрос:

У меня отношения «один ко многим». Parent Можно вместить множество Child сущностей. У детей есть собственность date , которая относится к типу Instant .

Я создал класс, который объединяет эти две сущности:

 data class Combined(
    @Embedded val parent: Parent,
    @Relation(
        parentColumn = "elementId",
        entityColumn = "Id"
    )
    val children: List<Child>
)
 

Я знаю, что могу извлечь все Combined элементы вот так:

 @Transaction
@Query("SELECT * FROM Parent")
fun getCombined(): Flow<List<Combined>>
 

Есть ли какой-либо способ получить List<Combined> , где включены только дети в определенном диапазоне дат?

Ответ №1:

Есть ли какой-либо способ получить список, в который включены только дети в определенном диапазоне дат?

Не так-то просто. @Relation работает, получая ВСЕХ потомков родителя на основе второго запроса (следовательно, рекомендуется использовать @Transaction). Фильтрация применяется только к выбору родителя.

Проблема в том, что если вы используете что-то вроде :-

 SELECT * FROM parent JOIN child ON child.Id = parent.elementId WHERE date BETWEEN fromDate and toDate;
 

Возвращается список, в котором каждая строка содержит родителя и одного потомка. Для извлечения родителя и всех отфильтрованных дочерних элементов требуется, чтобы список затем обрабатывался, чтобы выделить родителя с его дочерними элементами.

Основываясь на доступном коде, вот пример.

  • Одним исключением является столбец Id, который обычно является идентификатором ребенка, поэтому были добавлены столбцы ParentID, содержащие идентификатор родителя.
  • Другим исключением является то, что дата, для удобства/краткости, является просто строкой (ТЕКСТОМ), а не мгновенной, для которой требуется @TypeConverter.
  • Третье исключение состоит в том, что был использован сопутствующий объект (как будет показано в свое время).

Сначала новый POJO, фильтр для извлечения отфильтрованного списка (строка для каждого родителя и ребенка)

 data class Filter(
    @Embedded
    val parent: Parent,
    @Embedded
    val child: Child
)
 

Затем подходящий Dao будет отфильтрован для запроса (в значительной степени, как указано выше) :-

 @Query("SELECT * FROM parent JOIN child ON child.parentId = parent.elementId WHERE date BETWEEN :fromDate AND :toDate")
    abstract fun getFiltered(fromDate: String, toDate:String): List<Filter>
 
  • Обратите внимание, что это возвращает список фильтров, а не список объединенных фильтров

Последний ребенок с сопутствующим объектом, чтобы облегчить построение списка объединенных объектов из списка фильтров:-

 @Entity
data class Child(
    @PrimaryKey
    var id: Long? = null,
    var parentId: Long,
    @NotNull
    var childName: String,
    var date: String
) {

    companion object {

        fun buildParentsWithChildren(filter: List<Filter>): List<Combined> {
            var rv: ArrayList<Combined> = arrayListOf()
            if (filter.size < 1) return rv
            for (f: Filter in filter) {
                addChild(rv, getOrAddParent(rv, f), f)
            }
            return rv
        }

        private fun getOrAddParent(built: ArrayList<Combined>, f: Filter): Int {
            for (i in 0..(built.size-1)) {
                if (built.get(i).parent.parentName == f.parent.parentName) {
                    return i
                }
            }
            var newCombined: Combined = Combined(f.parent, emptyList())
            built.add(newCombined)
            return built.size - 1
        }

        private fun addChild(built: ArrayList<Combined>, parentIx: Int, f: Filter) {
            var currentChildren: ArrayList<Child> = arrayListOf<Child>()
            currentChildren.addAll(built.get(parentIx).children)
            currentChildren.add(f.child)
            built[parentIx] = Combined(parent = built.get(parentIx).parent, currentChildren)
        }
    }
}
 

Пример

Вот пример использования вышесказанного.

Сначала он строит некоторые данные, 3 родителя 5 детей (3 для первого родителя, 2 для второго родителя, 0 для третьего родителя) :-

введите описание изображения здесь

и

введите описание изображения здесь

Затем он использует запрос для извлечения некоторых данных и преобразует их в список. Затем он проходит по списку, выводимому в журнал.

Вот код из действия :-

     db = TheDatabase.getInstance(this)
    dao = db.getAllDao()

    var p1 = dao.insert(Parent(parentName = "Parent1"))
    var p2 = dao.insert(Parent(parentName = "Parent2"))
    var p3 = dao.insert(Parent(parentName = "Parent3"))

    dao.insert(Child(parentId = p1,childName = "Child1",date = "2000-01-01"))
    dao.insert(Child(parentId = p1,childName = "Child2",date = "2003-01-01"))
    dao.insert(Child(parentId = p1,childName = "Child3",date = "2005-01-01"))
    dao.insert(Child(parentId = p2,childName = "Child4",date = "2006-01-01"))
    dao.insert(Child(parentId = p2,childName = "Child5",date = "2007-01-01"))


    for(cmbnd: Combined in Child.buildParentsWithChildren(dao.getFiltered("2004-12-31","2010-01-01"))) {
        Log.d("DBINFO","Parent is ${cmbnd.parent.parentName}")
        for(c: Child in cmbnd.children)
            Log.d("DBINFO","Child is ${c.childName} date is ${c.date}")
    }
 

и результат :-

 2021-08-02 08:38:50.426 D/DBINFO: Parent is Parent1
2021-08-02 08:38:50.426 D/DBINFO: Child is Child3 date is 2005-01-01
2021-08-02 08:38:50.426 D/DBINFO: Parent is Parent2
2021-08-02 08:38:50.427 D/DBINFO: Child is Child4 date is 2006-01-01
2021-08-02 08:38:50.427 D/DBINFO: Child is Child5 date is 2007-01-01
 

т. е. только 1 из 3 детей от первого родителя, так как только 1 имеет дату между 2005 и 2009 годами. Однако оба дочерних элемента второго родителя соответствуют диапазону дат. Ничего для третьего родителя.

Комментарии:

1. Спасибо вам за исчерпывающий ответ! На самом деле я бы предпочел получить родителя 3 с пустым списком, вместо того, чтобы не возвращать его. Не могли бы вы, пожалуйста, изменить свой ответ, чтобы включить это?

2. @shko, это тогда не было бы запросом 1-много, так как нет никакой связи. Вам придется основывать его на извлечении списка родителей, а затем для каждого родителя извлекать ребенка для каждого родителя в соответствии с фильтром и пустым списком, если ни один из них не соответствует фильтру. На самом деле это должен быть другой вопрос, но я могу обновить ответ.

3. @shko Я добавил дополнительный ответ.

Ответ №2:

Дополнительный

что касается комментария:-

На самом деле я бы предпочел получить родителя 3 с пустым списком, вместо того, чтобы не возвращать его.

На самом деле, с точки зрения базы данных, это относительно сложно/сложно, поскольку вы просите рассмотреть «отсутствие отношений» (за неимением лучшего термина).

Однако, используя другой подход, то есть, чтобы получить всех родителей, а затем для каждого родителя, чтобы получить отфильтрованных детей (нет, если применимо).

Таким образом, внесение изменений в вышеизложенное, чтобы включить:-

в @Dao AllDao

  1. Добавьте запрос, чтобы получить всех родителей (у вас, вероятно, это есть) в виде списка.
  2. Добавьте запрос для получения отфильтрованного дочернего элемента в виде списка

напр. :-

 @Query("SELECT * FROM parent")
abstract fun getAllParents(): List<Parent>

@Query("SELECT * FROM child WHERE parentId = :parentId AND date BETWEEN :fromDate AND :toDate")
abstract  fun getFilteredChildrenForAParent(parentId: Long, fromDate: String, toDate: String): List<Child>
 

Если @Dao AllDao является абстрактным классом, а не интерфейсом, то.
3. Добавьте функцию, которая извлекает всех родителей, просматривая их, получая отфильтрованных детей, например :-

 fun getAllParentsWithFilteredChildren(fromDate: String, toDate: String): List<Combined> {
    var rv: ArrayList<Combined> = arrayListOf()
    for(p: Parent in this.getAllParents()) {
        rv.add(Combined(parent = p,this.getFilteredChildrenForAParent(p.elementId!!,fromDate, toDate)))
    }
    return rv
}
 

В противном случае (если вы не хотите, чтобы @Dao был абстрактным классом и, следовательно, интерфейсом) включите функцию в другом месте (в дочернем классе данных), такую как :-

     fun getAllParentsWithFilteredChildren(dao: AllDao, fromDate: String, toDate: String): List<Combined> {
        var rv: ArrayList<Combined> = arrayListOf()
        for (p: Parent in dao.getAllParents()) {
            rv.add(Combined(p,dao.getFilteredChildrenForAParent(p.elementId!!,fromDate,toDate)))
        }
        return rv
    }
 
  • ОБРАТИТЕ внимание на тонкое различие, dao передается функции

Результат

Внесение изменений в код деятельности в первом примере, чтобы включить :-

     for(cmbnd: Combined in dao.getAllParentsWithFilteredChildren("2004-12-31","2010-01-01")) {
        Log.d("DBINFOV2","Parent is ${cmbnd.parent.parentName}")
        for(c: Child in cmbnd.children)
            Log.d("DBINFOV2","Child is ${c.childName} date is ${c.date}")
    }

    for (cmbnd: Combined in Child.getAllParentsWithFilteredChildren(dao,"2004-12-31","2010-01-01")) {
        Log.d("DBINFOV3","Parent is ${cmbnd.parent.parentName}")
        for(c: Child in cmbnd.children)
            Log.d("DBINFOV3","Child is ${c.childName} date is ${c.date}")
    }
 

Затем результат (игнорируя результат из первого примера), затем logcat включает :-

 2021-08-03 08:33:30.812 D/DBINFOV2: Parent is Parent1
2021-08-03 08:33:30.812 D/DBINFOV2: Child is Child3 date is 2005-01-01
2021-08-03 08:33:30.812 D/DBINFOV2: Parent is Parent2
2021-08-03 08:33:30.812 D/DBINFOV2: Child is Child4 date is 2006-01-01
2021-08-03 08:33:30.812 D/DBINFOV2: Child is Child5 date is 2007-01-01
2021-08-03 08:33:30.812 D/DBINFOV2: Parent is Parent3


2021-08-03 08:33:30.817 D/DBINFOV3: Parent is Parent1
2021-08-03 08:33:30.817 D/DBINFOV3: Child is Child3 date is 2005-01-01
2021-08-03 08:33:30.817 D/DBINFOV3: Parent is Parent2
2021-08-03 08:33:30.817 D/DBINFOV3: Child is Child4 date is 2006-01-01
2021-08-03 08:33:30.817 D/DBINFOV3: Child is Child5 date is 2007-01-01
2021-08-03 08:33:30.817 D/DBINFOV3: Parent is Parent3
 
  • т. е. Родитель3, у которого нет детей, включен

Комментарии:

1. Спасибо тебе, Майк! Интересно, можно ли getAllParentsWithFilteredChildren вернуть Flow<List<Combined>> деньги . В конце концов, я хотел бы использовать LiveData для получения уведомлений об изменениях

2. @shko, конечно, это было бы/должно быть. Тем не менее, я никогда не выходил на арену LiveData, поэтому я не могу помочь в этой области, но я бы предположил, что это просто вопрос изменения подписи и возврата потока<rv>.