24

my question is an extension of this question (also mine :) ) -> Room composite Primary Key link to Foreign Key So, if I have this class:

public class FoodWithIngredients extends Food{

    @Relation(parentColumn = "id", entityColumn = "food_id", entity = 
    Ingredient.class)
    private List<Ingredient> mIngredients;

}

But the PrimaryKey of "Food" table is composite (primaryKeys = {"id", "language_id"}).

How I can make the @Relation returns records where "parentColumn = {"id", "language_id"}, entityColumn = {"food_id", food_language_id}" ?

Rajesh Pandya
  • 1,540
  • 4
  • 18
  • 31
MrVasilev
  • 1,503
  • 2
  • 17
  • 34
  • I'm having the same problem, I tried to link it creating an Index("food_id", "food_language_id", name = "relationHack") in both tables no luck. Only idea that comes up is to create a field in parent and child entity that concats the composite key data, although i don't know how to maintain the data of this field easily. Hope it helps!! – marcos E. Jan 30 '19 at 15:17
  • Have you found any solution? – LIFED Jun 18 '19 at 07:19
  • 1
    @marcosE. It's 2020 now. Have you guys found any canonical solution? Except the additional field solution (which is not that bad, actually) & the Java/Kotlin solutions. – ivan8m8 Jan 21 '20 at 14:08

3 Answers3

9

The annotation @Relation still doesn't offer support for composite primary keys.

The easiest way to fetch data querying multiple tables keeping the tables clean is with the @Embedded annotation. If you don't mind going dirty you could add an extra field where you concat the fields of the primary key the use @Relation over that field, with all the risks of maintaining the fields and potential wrong comparations over it's data. May be worth, dunno looks bad idea to me.

So the clean solution. Provided the next tables.

//Multiple Staff and Machine can participate on a WorkOrder and they do hours of work related to it

@Entity
data class Staff(
        @PrimaryKey val jdeNumber: String,
        val idNfc: String,
        val staffDescription: String,
        val triadorNumber: String,
        val approverId: Int)

@Entity(primaryKeys = ["machineId"])
data class Machine(
        val machineId: String,
        val machineNumber: String,
        val machineDescription: String,
        val machineNumberAux: String,
        val manufacturer: String,
        val model: String,
        val productionNumber: String,
        val hasHours: Boolean)

//A WorkOrder may have staff, machine or both
@Entity
data class WorkOrder(
        @PrimaryKey val woId: String,
        val date: Long,
        val comments: String = "",
        val userId: String,
        val partStatus: Int
)

//Embedded annotation creates all the fields from the entity inside these tables and add to the field name a prefix, then when we join tables we have no name conflict
@Entity(
        primaryKeys = ["woIdStaff", "wo_jdeNumber"],
        foreignKeys = [
                ForeignKey(entity = WorkOrder::class,
                        parentColumns = ["woId"],
                        childColumns = ["woIdStaff"],
                        onUpdate = ForeignKey.CASCADE,
                        onDelete = ForeignKey.RESTRICT)]
)
data class WorkOrderStaff(
        val woIdStaff: String,
        @Embedded(prefix = "wo_")
        val staff: Staff,
        val hourFrom: Long,
        val hourTo: Long,
        val hoursUsed: Long
)

@Entity(
        primaryKeys = ["woIdMachine", "wo_machineId"],
        foreignKeys = [
                ForeignKey(entity = WorkOrder::class,
                        parentColumns = ["woId"],
                        childColumns = ["woIdMachine"],
                        onUpdate = ForeignKey.CASCADE,
                        onDelete = ForeignKey.RESTRICT)]
)
data class WorkOrderMachine(
        val woIdMachine: String,
        @Embedded(prefix = "wo_")
        val machine: Machine,
        val hourFromMachine: Long,
        val hourToMachine: Long,
        val hoursUsedMachine: Long
)

//Important this entity is the one that maps from JOIN queries
data class FullWorkOrder(
        @Embedded
        val workOrder: WorkOrder
        @Embedded
        val staff: WorkOrderStaff?
        @Embedded
        val machine: WorkOrderMachine?
)

Then we want to query all the workOrders joined with the staff and machines that worked in them and the hours worked for each. So we write a query in our Dao.

@Query("select * from WorkOrder LEFT JOIN WorkOrderStaff ON woId = woIdStaff LEFT JOIN WorkOrderMachine ON woId = woIdMachine")
abstract fun getAllFullWorkOrders(): List<FullWorkOrder>

This mapping to the entity FullWorkOrder behaves like a Db query on a table visualization when you test a SQL, you have to map this so you don't duplicate rows of data or asign incorrectly data, depending the complexity of the joins. I recommend moving the data to key-value maps then join all togheter filtering duplicated keys. In this case we would be mapping to the entity we use on the UI -> DomainWorkOrder.

data class DomainWorkOrder(
    val id: String,
    .
    .
    .
    val staffList: List<StaffRow>
    val machineList: List<MachineRow>
)

I've taken out of the example the real complexity of the tables I'm using that's why you don't see any composite LEFT JOIN on the SQL. I have 8 tables anexed to WorkOrder(1-n) and 2 of those nest inside them 1-n relations down the road. I assure this would do the trick for most cases, just be careful if you try to join to the entity FullWorkOrder the Staff table to have the most recent data, I have a bad experience with that.

I know it's not pure but the schema is respected and the Query / mapping process do not require a lot of work and maintenance. Hope it helps!!

marcos E.
  • 477
  • 4
  • 11
  • Thank you for sharing. I as well used to concat the id from multiple fields which together provide the uniqueness. – ivan8m8 Jan 23 '20 at 12:03
  • @ivan8m8 Have you tried Embedded? What do you think about that approach? I'm looking for better ways to implement this, most of the apps I program have to support offline mode. – marcos E. Jan 23 '20 at 18:03
  • marcos E. Considering Room's @Relation still does not support references from a composite foreign key to a composite primary key, your solution seems all right. But the thing when you use a `Map<>` to go from `List` to `List` (right?) is kind of hacky. Btw, in my case I changed the schema so that it no longer has foreign keys. In that case it was possible just to move a small parent model into its child, so 1 of 2 tables was dropped. – ivan8m8 Jan 24 '20 at 07:56
  • @ivan8m8 this is the way data is extracted from SQL when joining tables as a standard for any relational database, so doesn't feel hacky to me, it's just that Relation does this work for you that's all. You can avoid this problem and optimize the nº of rows returned using some SQL tricks though(mostly subqueries), depends on your query, may not be worth the effort. – marcos E. Jan 27 '20 at 14:22
4

I have found some workaround. But it will effect on performance I suppose. You need to add special getter to your relation field which will filter the results with other parts of composite primary key.

In your case it will looks like:

public class FoodWithIngredients {

    @Embedded
    private Food food;

    @Relation(parentColumn = "id", entityColumn = "food_id", entity = 
    Ingredient.class)
    private List<Ingredient> mIngredients;

    public List<Ingredient> getIngredients() {
        List<Ingredient> result = List<Ingredient>();

        for (ingredient in mIngredients) {
            if (ingredient.foodLanguageId == food.languageId) {
                result.add(ingredient);   
            }
        }

        return result;
    }

}
LIFED
  • 467
  • 1
  • 4
  • 16
  • This is the workaround I had thought of, but it's gotta be highly inefficient on large datasets. How did it work out for you? – Pat Lee Jul 17 '21 at 10:25
  • 1
    @PatLee I don't actually remember where I used this, but that time it was the best solution I found. Didn't check how it works on large datasets, but of course it will take some time to filter the results. In my case it worked smooth enough. – LIFED Aug 05 '21 at 06:03
  • They implemented a proper way to do it three weeks ago, but it doesn't use `@Relation`. See my answer. – Pat Lee Aug 11 '21 at 11:07
  • 1
    @PatLee wow, didn't know, thank you for sharing this! – LIFED Aug 13 '21 at 08:01
1

Starting in Room 2.4.0-alpha04, you can write DAO methods returning a Map<Entity, List<Relation>> for 1:N relations. That enables you to write JOIN queries specifying how the relation shall be fetched, optionally specifying WHERE and ORDER BY clauses.

Source: https://issuetracker.google.com/issues/64247765

Pat Lee
  • 1,567
  • 1
  • 9
  • 13