#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
- Добавьте запрос, чтобы получить всех родителей (у вас, вероятно, это есть) в виде списка.
- Добавьте запрос для получения отфильтрованного дочернего элемента в виде списка
напр. :-
@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>.