Embed a many to one Room DAO object with multiple occurence of single entry in two different spots in the data hiearachy

huangapple go评论69阅读模式
英文:

Embed a many to one Room DAO object with multiple occurence of single entry in two different spots in the data hiearachy

问题

以下是您提供的内容的翻译:

我有以下的数据结构
Jobs 包括工人列表和海报列表
海报有一个工人。

我能够获取嵌入到Job对象中的工人和海报列表;但是我无法获取嵌入到海报对象中的单个工人。

我尝试使用关系和实体,但遇到一个问题,即实体不能包含关系,为了能够将某物包括为关系,它必须是一个实体。这意味着当我尝试使用一个名为PosterWithWorker的类将工人关系添加到海报对象时,JobsWithWorkersAndPosters会拒绝它,因为它不是一个实体,而且我不能将它变成一个实体。

我尝试使用外键将工人表映射到海报表。以下是这些类的示例:

@Entity(tableName = "poster")
data class Poster (
    @PrimaryKey val poster_id: Int,
    val job_id: Int,
    val qr_code: String,
    var status: Int,
    @ColumnInfo(name = "worker_id", index = true)
    val worker_id: Int,
    val longitude: Double,
    val latitude: Double
)
@Entity(tableName = "worker",
        foreignKeys = [
        androidx.room.ForeignKey(
            entity = Poster::class,
            parentColumns = ["poster_id"],
            childColumns = ["worker_id"],
            onDelete = CASCADE,
            onUpdate = CASCADE
        )])
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = "worker_id")
    val worker_id: Int,
    val workerName: String
)
data class PosterWithWorker(
        @Embedded val poster: Poster,
        @Relation(
            parentColumn = "poster_id",
            entityColumn = "worker_id"
        )
        val worker:  Worker
)
data class JobWithPostersAndWorkers(
    @Embedded val job: Job,
    @Relation(
        parentColumn = "job_id",
        entityColumn = "worker_id",
        associateBy = Junction(JobWorkerCrossRef::class)
    )
    val workers: List<Worker>,

    @Relation(entity=Poster::class, parentColumn = "job_id", entityColumn = "job_id")
    val poster: List<PosterWithWorker>
)

我遵循了这个示例https://stackoverflow.com/questions/70837760/android-room-3-one-to-many-relationship,并尝试遵循相同的嵌套模式。

我尝试了不同的变化并获得了不同的错误。

我的第一个问题是,是否可以在不使用外键的情况下完成这个任务,然后我做错了什么。

英文:

I have the following datastructure
Jobs includes a list of Workers and a list of Posters
Posters have a single worker.

I am able to get the list of workers and posters embedded into the Job object; but I am unable to get the single worker into the poster object.

I have tried using relations and entities but have run into a problem that entities cannot include relations, and in order to be able to include something as a relation it has to be an entity. That means that when I try to add the Worker relation to the Poster object using a PosterWithWorker class the JobsWithWorkersAndPosters rejects it because it isn't an Entity and I can't make it an entity.

I tried using Foreign keys to map the worker table into the poster table. Here is a snapshot of those classes

@Entity
    (tableName = &quot;poster&quot;)
data class Poster (
    @PrimaryKey val poster_id: Int,
    val job_id: Int,
    val qr_code: String,
    var status: Int,
    @ColumnInfo(name = &quot;worker_id&quot;, index = true)
    val worker_id: Int,
    val longitude: Double,
    val latitude: Double)
@Entity(tableName = &quot;worker&quot;,
        foreignKeys = [
        androidx.room.ForeignKey(
            entity = Poster::class,
            parentColumns = [&quot;poster_id&quot;],
            childColumns = [&quot;worker_id&quot;],
            onDelete = CASCADE,
            onUpdate = CASCADE
        )])
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = &quot;worker_id&quot;)
    val worker_id: Int,
    val workerName: String,

)
data class PosterWithWorker(
        @Embedded val poster: Poster,
        @Relation(
            parentColumn = &quot;poster_id&quot;,
            entityColumn = &quot;worker_id&quot;
        )
        val worker:  Worker
)
data class JobWithPostersAndWorkers(
    @Embedded val job: Job,
    @Relation(
        parentColumn = &quot;job_id&quot;,
        entityColumn = &quot;worker_id&quot;,
        associateBy = Junction(JobWorkerCrossRef::class)
    )
    val workers: List&lt;Worker&gt;,

    @Relation(entity=Poster::class, parentColumn = &quot;job_id&quot;, entityColumn = &quot;job_id&quot;)
    val poster: List&lt;PosterWithWorker&gt;
)
data class PosterWithWorker(
        @Embedded val poster: Poster,
        @Relation(
            parentColumn = &quot;poster_id&quot;,
            entityColumn = &quot;worker_id&quot;
        )
        val worker:  Worker
)

I was following this example https://stackoverflow.com/questions/70837760/android-room-3-one-to-many-relationship and tried to follow the same pattern of nesting.

I tried different variations and got different errors.

My first question is if this is possible to accomplish without using Foreign Keys and then what am I doing wrong.

答案1

得分: 1

> 我的第一个问题是,如果不使用外键是否有可能完成这个任务,那么我做错了什么。

答案中包括:

> 外键是可选的,但可以强制执行并有助于维护引用完整性。在外键中,onDelete 和 onUpdate 是可选的

SQLite中的外键是Room的包装,是一个约束(规则),其中应用外键约束的列(子列)中的值必须是引用列(父列)中存在的值。也就是所谓的引用完整性。另一种思考方式是,外键约束禁止孤儿。

在您的情况下(根据评论 我想向海报类添加一个工作人员字段。每个海报只有一个工作人员 ),通过以下方式:

@Entity(tableName = "worker",
        foreignKeys = [
        androidx.room.ForeignKey(
            entity = Poster::class,
            parentColumns = ["poster_id"],
            childColumns = ["worker_id"],
            onDelete = CASCADE,
            onUpdate = CASCADE
        )])
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = "worker_id")
    val worker_id: Int,
    val workerName: String
)

您实际上是在说 worker_id 列必须是 Poster 表的 poster_id 列中存在的值。从而限制了工作人员的数量与海报的数量相同。

我认为您想要的是:

@Entity(
    tableName = "poster",
    foreignKeys = [
        ForeignKey(
            entity = Worker::class,
            parentColumns = ["worker_id"],
            childColumns = ["worker_id"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class Poster (
    @PrimaryKey val poster_id: Int,
    val job_id: Int,
    val qr_code: String,
    var status: Int,
    @ColumnInfo(name = "worker_id", index = true)
    val worker_id: Int,
    val longitude: Double,
    val latitude: Double
)
  • 注意 Worker 类中被注释的外键。

也就是说,海报包含对单个工作人员的引用(该工作人员必须存在)。这意味着海报是工作人员的子集。一个工作人员可以有许多孩子(海报)。

附加信息

关于工作包含工作人员列表和海报列表,海报只有一个工作人员的问题,评论中有一些矛盾。

> 实际上这里存在一个混淆,理想情况下,一个海报不需要一个工作人员。

您还提到

> 我尝试了您的解决方案,但我收到了 FOREIGN KEY 约束失败(错误代码 787 SQLITE_CONSTRAINT_FOREIGNKEY)的错误。

这是因为父表中没有空行,因此如果尝试引用没有海报的情况,将违反规则。

在定义外键时,定义了外键的列将成为子列,而引用的列将成为父列。

在这种情况下,有一个解决方案,允许一个工作有许多(0-数百万)工作人员,同时相同的工作人员可以参与许多工作。这是一种多对多关系。它还满足了尴尬的海报-工作人员,并满足了引用完整性(外键)。

因此,您可以从3个核心表开始:Job、Worker 和 Poster。前两个独立于其他表。例如:

@Entity
data class Job(
    @PrimaryKey
    val job_id: Long? = null,
    val jobName: String
)
@Entity
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = "worker_id")
    val worker_id: Long? = null,
    val workerName: String
)

由于海报必须具有工作作为其父级,并且一个工作可以有多个海报(即,一堆海报),这是一对多关系。可以使用外键来强制执行引用完整性,即海报不能在没有父项的情况下存在。所以:

@Entity(
    foreignKeys = [
        /* 海报必须是作业的子项(没有作业没有海报)*/
        ForeignKey(
            entity = Job::class,
            parentColumns = ["job_id"],
            childColumns = ["job_id_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class Poster(
    @PrimaryKey
    val poster_id: Long? = null,
    @ColumnInfo(index = true)
    val job_id_map: Long, /* 作业的子项 */
    val qr_code: String,
    var status: Int,
    val longitude: Double,
    val latitude: Double
)

接下来是每个工作的工作人员列表,以及工作人员可以在多个工作上工作的假设,因此是多对多关系,因此需要一个中间/交叉引用/映射/联合...表。

所以:

@Entity(
    primaryKeys = ["job_id_map","worker_id_map"], /* 复合主键 */
    foreignKeys = [
        ForeignKey(
            entity = Job::class,
            parentColumns = ["job_id"],
            childColumns = ["job_id_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Worker::class,
            parentColumns = ["worker_id"],
            childColumns = ["worker_id_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class JobWorkerMap(
    val job_id_map: Long,
    @ColumnInfo(index = true)
    val worker_id_map: Long
)
  • 这已经在先前进行了覆盖。

此时,海报的工作人员已被省略。因此,可以出现没有工作人员的情况。但为了允许海报的工作人员,可以使用一个映射表(0-多个),但需要限制为单个工作人员。这可以通过将主键仅放在海报列而不是两列的组合上来实现。所以

英文:

> My first question is if this is possible to accomplish without using Foreign Keys and then what am I doing wrong.

Yes, the referenced answer includes:-

> foreignKeys are optional but enforce and help maintain referential integrity. Within foreign keys the onDelete and onUpdate are optional

A Foreign Key, in SQLite, which is what Room is a wrapper around, is a constraint (a rule) where the value in the column(s) to which the FK constraint is applied to (the CHILD) MUST be a value that exists in the column(s) that is(are) referenced (the PARENT). i.e known as Referential Integrity. Another way of thinking of this is that Foreign Key constraints prohibit Orphans.

In your scenario (and according to the comment I'd like to add a worker field to the poster class. There will only be one worker per poster.)

By using:-

@Entity(tableName = &quot;worker&quot;,
        foreignKeys = [
        androidx.room.ForeignKey(
            entity = Poster::class,
            parentColumns = [&quot;poster_id&quot;],
            childColumns = [&quot;worker_id&quot;],
            onDelete = CASCADE,
            onUpdate = CASCADE
        )])
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = &quot;worker_id&quot;)
    val worker_id: Int,
    val workerName: String,

)

You are affectively saying that the worker_id column MUST be an existing value in the poster_id column of the Poster table. Thus limiting the number of workers to the number of Posters.

I believe what you want is :-

@Entity(
    tableName = &quot;poster&quot;,
    foreignKeys = [
        ForeignKey(
            entity = Worker::class,
            parentColumns = [&quot;worker_id&quot;],
            childColumns = [&quot;worker_id&quot;],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class Poster (
    @PrimaryKey val poster_id: Int,
    val job_id: Int,
    val qr_code: String,
    var status: Int,
    @ColumnInfo(name = &quot;worker_id&quot;, index = true)
    val worker_id: Int,
    val longitude: Double,
    val latitude: Double)

@Entity(
    tableName = &quot;worker&quot;,
    /*
    foreignKeys = [
        androidx.room.ForeignKey(
            entity = Poster::class,
            parentColumns = [&quot;poster_id&quot;],
            childColumns = [&quot;worker_id&quot;],
            onDelete = CASCADE,
            onUpdate = CASCADE
        )]*/
)
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = &quot;worker_id&quot;)
    val worker_id: Int,
    val workerName: String,
    )

data class PosterWithWorker(
    @Embedded val poster: Poster,
    @Relation(
        parentColumn = &quot;worker_id&quot;,
        entityColumn = &quot;worker_id&quot;
    )
    val worker:  Worker
)
  • Note the commented out FKey in the Worker class.

i.e. The Poster includes a reference to the single Worker (who must exist). That is the Poster is (Posters are) the child of the Worker. A Worker may have many children (Posters).

Additional

re

> Jobs includes a list of Workers and a list of Posters, Posters have a single worker.

Which is then contradicted by the comment

> Actually there is another confusion here ideally a poster doesn't need a worker.

You also comment

> I tried your solution and I'm getting a FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY) error.

This because the are no rows that are null in the parent table thus RULE broken if you try to have a reference to no Poster.

When defining Foreign Keys then the column where the FK is defined will be the child and the referenced column will be the parent.

Saying that here is a solution that allows a Job to have many (0-millions of) workers and that the same worker can part of many Jobs. a many-many relationship. It also caters for the Awkward Poster-Worker but also caters for Referential Integrity (Foreign Keys).

So you can start of with 3 core tables Job, Worker and Poster. The first two are independent of other tables. e.g.

@Entity
data class Job(
    @PrimaryKey
    val job_id: Long?=null,
    val jobName: String
)
@Entity
data class Worker(
    @PrimaryKey
    @ColumnInfo(name = &quot;worker_id&quot;)
    val worker_id: Long?=null,
    val workerName: String,
)

As a Poster MUST have a Job as it's parent and a Job could have Many Posters (i.e. a list of posters). This is a 1 (Job) to many (Poster). A Foreign Key may be used to enforce Referential Integrity. i.e. a Poster cannot exist without a parent. So:-

@Entity(
    foreignKeys = [
        /* POSTER MUST be a child of a Job (no Job no poster)*/
        ForeignKey(
            entity = Job::class,
            parentColumns = [&quot;job_id&quot;],
            childColumns = [&quot;job_id_map&quot;],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class Poster(
    @PrimaryKey
    val poster_id: Long?=null,
    @ColumnInfo(index = true)
    val job_id_map: Long, /* CHILD OF Job */
    val qr_code: String,
    var status: Int,
    val longitude: Double,
    val latitude: Double
)

Next the list of Workers per Job and the assumption that a Worker could be a Worker on many jobs and thus a many0many relationship and thus an intermediate/cross reference/mapping/associative .... table.

So:-

@Entity(
    primaryKeys = [&quot;job_id_map&quot;,&quot;worker_id_map&quot;], /* composite primary key */
    foreignKeys = [
        ForeignKey(
            entity = Job::class,
            parentColumns = [&quot;job_id&quot;],
            childColumns = [&quot;job_id_map&quot;],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Worker::class,
            parentColumns = [&quot;worker_id&quot;],
            childColumns = [&quot;worker_id_map&quot;],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class JobWorkerMap(
    val job_id_map: Long,
    @ColumnInfo(index = true)
    val worker_id_map: Long,
)
  • this already covered previously.

At this stage the Poster's Worker has been omitted. So the no Worker situation is possible. However, to allow for a Worker a mapping table can be used (0-many) BUT the need is to limit to a single worker if one. This can be accomplished by having the primary key on just the Poster column rather than on a composite of both columns. So:-

@Entity
data class PosterWorkerRestrictedMap(
    @PrimaryKey /*Primary Key restricts to 1 poster */
    val poster_id_map: Long,
    @ColumnInfo(index = true)
    val worker_id_map: Long
)

To support the inclusion of this 0 or 1 Worker for a Poster then:-

data class PosterWithWorkerOrNot(
    @Embedded
    val poster: Poster,
    @Relation(
        entity = Worker::class,
        parentColumn = &quot;poster_id&quot;,
        entityColumn = &quot;worker_id&quot;,
        associateBy = Junction(
            value = PosterWorkerRestrictedMap::class,
            parentColumn = &quot;poster_id_map&quot;,
            entityColumn = &quot;worker_id_map&quot;
        )
    )
    val worker: List&lt;Worker&gt; /* List should be empty or have 1 element */
)

Finally to cater for the Job with it's Workers and with it's PostersWithWorkerOrNot then:-

data class JobWithWorkersAndPosterWithPosterWorker(
    @Embedded
    val job:Job,
    @Relation(
        entity = Worker::class,
        parentColumn = &quot;job_id&quot;,
        entityColumn = &quot;worker_id&quot;,
        associateBy = Junction(
            value = JobWorkerMap::class,
            parentColumn = &quot;job_id_map&quot;,
            entityColumn = &quot;worker_id_map&quot;
        )

    )
    val workers: List&lt;Worker&gt;,
    @Relation(
        entity = Poster::class,
        parentColumn = &quot;job_id&quot;,
        entityColumn = &quot;job_id_map&quot;
    )
    val posters: List&lt;PosterWithWorkerOrNot&gt;
)

To demonstrate some DAO functions:-

@Dao
interface TheDAOs {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(worker: Worker): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(poster: Poster): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(job: Job): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(jobWorkerMap: JobWorkerMap): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(posterWorkerRestrictedMap: PosterWorkerRestrictedMap): Long

    @Transaction
    @Query(&quot;SELECT * FROM job&quot;)
    fun getAllJobsWithWorkersAndPostersWithPosterWorker(): List&lt;JobWithWorkersAndPosterWithPosterWorker&gt;
}
  • i.e. the ability to insert into all of the tables and also the query to extract Jobs with the list of Workers with the list of Posters each of which may or may not have a Poster Worker.

To actually demonstrate the following:-

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

    val w1id = dao.insert(Worker(null,&quot;W1&quot;))
    val w2id = dao.insert(Worker(null,&quot;W2&quot;))
    val w3id = dao.insert(Worker(null,&quot;W3&quot;))
    val w4id = dao.insert(Worker(null,&quot;W4&quot;))
    val w5id = dao.insert(Worker(null,&quot;W5&quot;))

    val j1id = dao.insert(Job(null,&quot;J1&quot;))
    val j2id = dao.insert(Job(null,&quot;J2&quot;))
    val j3id = dao.insert(Job(null,&quot;J3&quot;))

    val p1id = dao.insert(Poster(null,j1id,&quot;QRXXX&quot;,1,10.132,20.78))
    val p2id = dao.insert(Poster(null,j2id,&quot;QRYYY&quot;,2,1.333,12.765))
    val p3id = dao.insert(Poster(null,j2id,&quot;QRZZZ&quot;,3,2.456,13.675))
    val p4id = dao.insert(Poster(null,j1id,&quot;QRAAA&quot;,2,3.213,14.902))

    dao.insert(JobWorkerMap(j1id,w2id))
    dao.insert(JobWorkerMap(j1id,w3id))
    dao.insert(JobWorkerMap(j1id,w1id))
    dao.insert(JobWorkerMap(j2id,w4id))
    dao.insert(JobWorkerMap(j2id,w5id))
    dao.insert(JobWorkerMap(j2id,w3id))
    dao.insert(JobWorkerMap(j2id,w1id))
    logAll(&quot;_R1&quot;)

    dao.insert(PosterWorkerRestrictedMap(p1id,w2id))
    dao.insert(PosterWorkerRestrictedMap(p2id,w4id))
    dao.insert(PosterWorkerRestrictedMap(p3id,w5id))
    logAll(&quot;_R2&quot;)


}

fun logAll(suffix: String) {
    for (jwwapwpw in dao.getAllJobsWithWorkersAndPostersWithPosterWorker()) {
        val sbwl = StringBuilder()
        val sbpl = StringBuilder()
        for (w in jwwapwpw.workers) {
            sbwl.append(&quot;\n\tWorker is ${w.workerName} ID is ${w.worker_id}&quot;)
        }
        for (p in jwwapwpw.posters) {
            sbpl.append(&quot;\n\tPoster is ${p.poster.qr_code}&quot;)
            if (p.worker.size &gt; 0) {
                sbpl.append(&quot; PosterWorker is ${p.worker[0].workerName} ID is ${p.worker[0].worker_id}&quot;)
            } else {
                sbpl.append(&quot; NO POSTER WORKER&quot;)
            }
        }
        Log.d(&quot;DBINFO${suffix}&quot;,&quot;JOB IS ${jwwapwpw.job.jobName} it has ${jwwapwpw.workers.size} Workers and ${jwwapwpw.posters.size}. posters. They are${sbwl}${sbpl} &quot;)
    }
}

Result the output to the log:-

2023-03-07 16:50:29.911 D/DBINFO_R1: JOB IS J1 it has 3 Workers and 2. posters. They are
    	Worker is W1 ID is 1
    	Worker is W2 ID is 2
    	Worker is W3 ID is 3
    	Poster is QRXXX NO POSTER WORKER
    	Poster is QRAAA NO POSTER WORKER 
2023-03-07 16:50:29.911 D/DBINFO_R1: JOB IS J2 it has 4 Workers and 2. posters. They are
    	Worker is W1 ID is 1
    	Worker is W3 ID is 3
    	Worker is W4 ID is 4
    	Worker is W5 ID is 5
    	Poster is QRYYY NO POSTER WORKER
    	Poster is QRZZZ NO POSTER WORKER 
2023-03-07 16:50:29.911 D/DBINFO_R1: JOB IS J3 it has 0 Workers and 0. posters. They are 



2023-03-07 16:50:29.932 D/DBINFO_R2: JOB IS J1 it has 3 Workers and 2. posters. They are
    	Worker is W1 ID is 1
    	Worker is W2 ID is 2
    	Worker is W3 ID is 3
    	Poster is QRXXX PosterWorker is W2 ID is 2
    	Poster is QRAAA NO POSTER WORKER 
2023-03-07 16:50:29.932 D/DBINFO_R2: JOB IS J2 it has 4 Workers and 2. posters. They are
    	Worker is W1 ID is 1
    	Worker is W3 ID is 3
    	Worker is W4 ID is 4
    	Worker is W5 ID is 5
    	Poster is QRYYY PosterWorker is W4 ID is 4
    	Poster is QRZZZ PosterWorker is W5 ID is 5 
2023-03-07 16:50:29.932 D/DBINFO_R2: JOB IS J3 it has 0 Workers and 0. posters. They are
  • Note that R1 is before any Workers have been assigned to any of the Posters. R2 is after some Workers have been assigned. Hence the output has been split to make it easier to see.
  • As can be seen Job J1 has 2 Posters one with and the other without a worker. Potential issues with nulls have been avoided as has the FK error for when there is no Worker for a Poster.

huangapple
  • 本文由 发表于 2023年3月7日 05:50:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/75656155.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定