2

I have a one to many relationship. One Parent can hold many Child entities. The children have the property date, which is of type Instant.

I have made a class which combines these two entities:

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

I know that I can retrieve all Combined elements like so:

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

Is there any way to retrieve a List<Combined> where only the children within a certain date range is included?

shko
  • 64
  • 2
  • 20

2 Answers2

2

Is there any way to retrieve a List where only the children within a certain date range is included?

Not that easily. @Relation works by getting ALL the children of the parent based upon a second query (hence why @Transaction is recommended). Filtering only applies to the selection of the parent.

The problem is that if you use something like :-

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

What is returned is a list where each row contains the parent and the single child. To extract the parent and all the filtered children requires the list to then be processed to single out the parent's with it's children.

Based upon the available code, here's an example.

  • One exception is the Id column which would typically be the ID of the child, so a parentId columns has been added that hold the ID of the parent.

  • Another exception is that the date, for convenience/brevity, is just a String (TEXT) rather than Instant which requires an @TypeConverter.

  • A Third exception is that a companion object has been used (as will be shown in due course).

First a new POJO, Filter for the extraction of the filtered list (a row for each parent and child)

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

Next a suitable Dao getFiltered for the query (pretty much as above) :-

@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>
  • Note that this returns a List of Filters NOT a List of Combineds

Last the Child with the companion object to facilitate building a List of Combineds from the List of Filters:-

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

Example

Here's an example of using the above.

First it builds some data, 3 parents 5 children (3 to the first parent, 2 to the second parent, 0 to the third parent) :-

enter image description here

and

enter image description here

It then uses the query to extract some data and converts this to a List. It then traverses the list outputting to the Log.

Here's the code from an Activity :-

    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}")
    }

and the result :-

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

i.e. Only 1 of the 3 children from the first parent as only 1 has a date between 2005 and 2009. However, both child of the second parent as they fit the date range. Nothing for the third parent.

MikeT
  • 51,415
  • 16
  • 49
  • 68
  • Thank you for the comprehensive answer! I would actually prefer to get Parent 3 with an empty list, instead of not returning it. Could you please modify your answer to incorporate this? – shko Aug 02 '21 at 14:31
  • @shko, that would then not be a query of a 1-many as there is no relationship. You would have to base it on extracting a list of Parent's and then for each parent extracting the Child's per parent according to the filter and an empty list if none match the filter. Really that should be a different question, but I may update the answer. – MikeT Aug 02 '21 at 22:02
  • @shko I've added a supplementary answer. – MikeT Aug 02 '21 at 22:47
2

Additional

regarding the comment:-

I would actually prefer to get Parent 3 with an empty list, instead of not returning it.

This is actually, database wise, is relatively hard/complicated as you are asking for a "non-relationship" (for want of a better term) to be considered.

However, using a different approach, that is to get All parents and then for each parent to get the filtered children (none if applicable).

As such amending the above to include:-

in the @Dao AllDao

  1. Add a Query to get All Parents (you probably have this) as a List.
  2. Add a Query to get the Filtered Child as a List

e.g. :-

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

If the @Dao AllDao is an abstract class rather than an interface then. 3. Add a function that extracts all the parents, looping through them getting the filtered children e.g. :-

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
}

Otherwise (if you don't want the @Dao to be an abstract class and therefore an interface) include a function, elsewhere (in the Child data class) such as :-

    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
    }
  • NOTE the subtle difference, the dao is passed to the function

Result

Amending the activity code in the first example to include :-

    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}")
    }

Then the result (ignoring the result from the first example) then the logcat includes :-

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
  • i.e. Parent3 with no children is included
MikeT
  • 51,415
  • 16
  • 49
  • 68
  • Thank you Mike! I wonder if it is possible to make `getAllParentsWithFilteredChildren` return `Flow>` . In the end I would like to use LiveData to get notified about changes – shko Aug 03 '21 at 16:35
  • @shko Of course it would/should. However, I have never ventured into the LiveData arena, so I can't assist in that area, but I'd guess that it's just a matter of changing the signature and returning a Flow. – MikeT Aug 03 '21 at 20:20