【问题标题】:Relational room database: The class must be either entity or database view关系室数据库:类必须是实体或数据库视图
【发布时间】:2021-06-04 21:08:24
【问题描述】:

我正在尝试了解如何将 Room 与关系表一起使用。我创建了一个 Job 模型,它有一个位置列表,因此需要 Job 和 Location 对象之间的一对多关系。为此,我创建了一个 JobWrapper 数据类来保存 Job 和位置。但是,在构建时出现以下错误:

类必须是@Entity 或@DatabaseView。 - java.util.Collectionerror:实体和 POJO 必须有一个可用的 公共构造函数。你可以有一个空的构造函数或构造函数 其参数与字段匹配(按名称和类型)。 - java.util.Collection \models\JobWrapper.java:12:错误:找不到 java.util.Collection 中的子实体列parentId。 选项: 私有 java.util.Collection 位置; 公共最终类 JobWrapper { ^ 尝试了以下构造函数,但它们未能匹配:
JobWrapper(models.Job,java.util.Collection) -> [参数:作业-> 匹配字段:作业,参数:位置-> 匹配字段:不匹配] 模型\JobWrapper.java:9: 错误:找不到字段的设置器。

我注意到它至少找不到位置表。但是,我不知道如何处理这个问题。从数据库读取时没有出现该问题 - 当我尝试使用 JobDAO 将数据放入数据库时​​,它首次出现。我已经花了一天时间尝试解决它,因此正在寻找解决方案或有关如何解决它的一些建议。

注意:我一直遵循以下指南:

  1. https://developer.android.com/training/data-storage/room/relationships#one-to-many
  2. https://dev.to/normanaspx/android-room-how-works-one-to-many-relationship-example-5ad0

下面是我项目中的一些相关代码sn-ps:

JobWrapper.kt

data class JobWrapper(
    @Embedded val job: Job,

    @Relation(
        parentColumn = "jobid",
        entityColumn = "parentId"
    ) var locations : Collection<Location>
)

工作

@Entity
data class Job (
    @PrimaryKey
    @NonNull
    var jobid : String,

    @NonNull
    @ColumnInfo(name = "job_status")
    var status : JobStatus,

    @NonNull
    @SerializedName("createdByAuth0Id")
    var creator : String,

    @SerializedName("note")
    var note : String?,

    @NonNull
    var organisationId : String,

    @NonNull
    var type : JobType,

    @SerializedName("atCustomerId")
    @NonNull
    @ColumnInfo(name = "working_at_customer_id")
    var workingAtCustomerId : String,

    @SerializedName("toCustomerId")
    @NonNull
    @ColumnInfo(name = "working_to_customer_id")
    var workingToCustomerId : String,
)

JobStatus.kt

enum class JobStatus {
    CREATED,
    READY,
    IN_PROGRESS,
    FINISHED
}

Location.kt

@Entity
data class Location (
    @PrimaryKey(autoGenerate = true)
    var entityId: Long,

    @NonNull
    var parentId: String,

    @NonNull
    var locationId: String,

    @NonNull
    var type: String
) {
    constructor() : this(0, "", "", "")
}

JobDao.kt

@Dao
interface JobDAO {
    @Transaction
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(job: JobWrapper)

    @Transaction
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(jobs: List<JobWrapper>)

    @Transaction
    @Update
    fun update(job: JobWrapper)

    @Transaction
    @Delete
    fun delete(job: JobWrapper)

    @Transaction
    @Query("DELETE FROM Job")
    fun deleteAll()

    @Transaction
    @Query("SELECT * FROM Job")
    fun getAll(): LiveData<List<JobWrapper>>
}

【问题讨论】:

    标签: android kotlin android-room


    【解决方案1】:

    正如 Kraigolas 所指出的,您只能直接使用 JobWrapper 来提取您需要通过实际底层实体插入/删除/更新的数据。

    考虑以下

    • (与 Kraigolas 的解决方案不同,扩展功能在 JobDao 中而不是在存储库中(swings 和 roundabouts 争论哪个更好))

    • 注意测试为了简洁和方便,我做了一些更改,所以你必须修改以适应。

    JobDao

    @Dao
    interface JobDAO {
    
        /* Core/Underlying DAO's */
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insert(job: Job): Long
        @Insert()
        fun insert(location: List<Location>): List<Long>
        @Transaction
        @Delete
        fun delete(location: List<Location>)
        @Delete
        fun delete(job: Job)
        @Update
        fun update(job: Job)
        @Update
        fun update(location: List<Location>)
    
        @Transaction
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insert(job: JobWrapper): Long {
            val rv =insert(job.job)
            insert(job.locations)
            return rv
        }
    
        @Transaction
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insertAll(jobs: List<JobWrapper>) {
            for (jw: JobWrapper in jobs) {
                insert(jw)
            }
        }
    
        @Transaction
        @Update
        fun update(job: JobWrapper) {
            update(job.locations)
            update(job.job)
        }
    
        /* Delete according to JobWrapper allowing optional deletion of locations */
        @Transaction
        @Delete
        fun delete(job: JobWrapper, deleteChildLocations: Boolean) {
            if (deleteChildLocations) {
                delete(job.locations)
            }
            delete(job.job)
        }
    
        /* will orphan locations as is */
        /* Note using Foreign Keys in Location (to Job) with ON DELETE CASCADE */
        @Transaction
        @Query("DELETE FROM Job")
        fun deleteAll()
    
        @Transaction
        @Query("SELECT * FROM Job")
        fun getAll(): List<JobWrapper>
    
        @Transaction
        @Query("SELECT * FROM job WHERE jobid = :jobid")
        fun getJobWrapperByJobId(jobid: String ): JobWrapper
    }
    
    • 可以看出,Core Dao 包括 Job 和 Location 操作
    • JobWrapper 操作调用核心操作
    • 为方便起见,使用列表代替集合(也就是我只涉足 Kotlin)
    • 为了方便起见,类型已更改为使用 String 代替 JobType

    正如我已经测试过的那样,使用的演示/示例如下(显然 JobDao 同上)

    使用的 POJO 和实体是/曾经是:-

    JobWrapper

     data class JobWrapper(
        @Embedded val job: Job,
    
        @Relation(
            parentColumn = "jobid",
            entityColumn = "parentId",
            entity = Location::class
        ) var locations : List<Location>
    )
    
    • 列表而不是集合

    工作

    @Entity
    data class Job (
        @PrimaryKey
        @NonNull
        var jobid : String,
    
        @NonNull
        @ColumnInfo(name = "job_status")
        var status : String,
    
        @NonNull
        var creator : String,
    
        var note : String?,
    
        @NonNull
        var organisationId : String,
    
        @NonNull
        var type : String,
    
        @NonNull
        @ColumnInfo(name = "working_at_customer_id")
        var workingAtCustomerId : String,
    
        @NonNull
        @ColumnInfo(name = "working_to_customer_id")
        var workingToCustomerId : String,
    )
    
    • 大致相同,但为方便起见,使用字符串而不是对象

    位置

    @Entity(
        foreignKeys = [
            ForeignKey(
                entity = Job::class,
                parentColumns = ["jobid"],
                childColumns = ["parentId"],
                onUpdate = ForeignKey.CASCADE,
                onDelete = ForeignKey.CASCADE
            )
        ])
    data class Location (
        @PrimaryKey(autoGenerate = true)
        var entityId: Long,
    
        @NonNull
        var parentId: String,
    
        @NonNull
        var locationId: String,
    
        @NonNull
        var type: String
    ) {
        constructor() : this(0, "", "", "")
    }
    
    • 为参照完整性添加外键并删除 CASCADE(UPDATE CASCADE 并非真正需要,除非您更改作业 ID,其他更新不会级联(也不需要))

    @Database 使用 JobDatabase

    @Database(entities = [Location::class,Job::class],version = 1)
    abstract class JobDatabase: RoomDatabase() {
        abstract fun getJobDao(): JobDAO
        
        companion object {
            var instance: JobDatabase? = null
            fun getInstance(context: Context): JobDatabase {
                if (instance == null) {
                    instance = Room.databaseBuilder(context, JobDatabase::class.java, "job.db")
                        .allowMainThreadQueries()
                        .build()
                }
                return instance as JobDatabase
            }
        }
    }
    
    • allowMainThreadQueries 用于允许在主线程上进行测试

    测试/演示活动MainActivity

    class MainActivity : AppCompatActivity() {
    
        lateinit var db: JobDatabase
        lateinit var dao: JobDAO
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            db = JobDatabase.getInstance(this)
            dao = db.getJobDao()
    
            var jobId: String = "Job1"
            var jw = JobWrapper(
                Job(
                    jobId,
                    "x",
                    "Fred",
                    "Note for Job1",
                    "Org1",
                    "A","Cust1",
                    "Cust1"),
                listOf(
                    Location(0,jobId,"loc1","J"),
                    Location(0,jobId,"Loc2","K"),
                    Location(0,jobId,"Loc3","L")
                )
            )
            dao.insert(jw)
            dao.insertAll(createJobWrapperList(10))
            dao.delete(dao.getJobWrapperByJobId("job6"),true)
            var jobWrapper = dao.getJobWrapperByJobId("job7")
            jobWrapper.job.creator = "update creator"
            for (l in jobWrapper.locations) {
                if (l.type == "M") l.type= "UPDATED"
            }
            dao.update(jobWrapper)
        }
    
        fun createJobWrapperList(numberToCreate: Int): List<JobWrapper> {
            val l = mutableListOf<JobWrapper>()
            for(i in 1..numberToCreate) {
                var jid = "job$i"
                l.add(
                    JobWrapper(
                    Job(jid,"X","Creator$i","Note for $jid","org$jid","T","custA","custT"),
                        arrayListOf(
                            Location(0,jid,"loc_$jid.1","L"),
                            Location(0,jid,"loc_$jid.2","M"),
                            Location(0,jid,"loc_$jid.3","N")
                        )
                    )
                )
            }
            return l.toList()
        }
    }
    

    这个:-

    1. 获取数据库实例和 dao。
    2. 通过单个 JobWrapper 添加作业及其位置
    3. 通过createJobWrapperList 函数生成的 JobWrappers 列表为每个作业添加 x (10) 个作业和 3 个位置。 4.删除通过getJobWrapperByJobId获得的JobWrapper,包括使用delete删除与jobid“job6”关联的JobWrapper的底层位置(true)。
    4. 获取与“job7”关联的 JobWrapper 更改创建者并将类型为“M”的位置更改为“已更新”(只是一个),然后使用 update(JobWrapper) 来应用更新。

    警告

    使用 JobWrapper 插入,因为它具有 REPLACE 冲突策略,如果它替换将导致额外的位置,因为将始终生成 entityId。

    结果

    运行上述结果:-

    工作表:-

    • 可以看出,job6 已被删除(添加了 11 行,左 10 行),并且 job7 的创建者已更新。

    位置表:-

    • 可以看出,没有 job6 个位置(以前是 33 个位置 (11 * 3) 现在是 30 个),并且类型 M 的位置已根据传递的 JobWrapper 进行了更新。

    你问过:-

    在插入子项(位置)时,如何确保与正确的父项(作业)的关系?

    我认为以上说明了如何。

    【讨论】:

    • 非常好的答案,非常感谢您所做的所有额外努力:)我会立即尝试:D
    • 这很好。但是,我仍然不完全了解该位置如何获得其父工作的参考。我收到此错误:NOT NULL 约束失败:Location.parent_id (code 1299 SQLITE_CONSTRAINT_NOTNULL) 我最好的猜测是 JobDao.kt 中缺少某些东西
    • 哦,我明白了,您是在创建函数中手动将其添加到对象中。
    • @7heViking 是的,要构建一个用于插入的 JobWrapper,您必须预先知道 jobid。所以你只需使用它。
    【解决方案2】:

    JobWrapper 没有与之关联的表。请注意,从您的数据库中提取是很好的,因为它将从LocationJob 表中获取,但是使用JobWrapper 插入数据库是没有意义的,因为没有与之关联的表。

    相反,您需要分别插入Jobs 和Locations。由于jobidparentid关联,数据库可以一起查询,所以你不用担心他们失去了彼此的关系,你仍然可以查询你的JobWrapper

    如果按照我上面的解释,你仍然认为你应该能够插入JobWrapper,那么你可以使用如下方法创建一个存储库:

    suspend fun insertWrapper(wrapper : JobWrapper){
        dao.insertJob(wrapper.job)
        dao.insertLocations(wrapper.locations)
    }
    

    insertJob 插入单个JobinsertLocations 插入Location 的列表。

    【讨论】:

    • 好吧,这听起来是一个解决方案。就一个问题。在插入子项(位置)时,如何确保与正确的父项(工作)的关系?
    • @7heViking 在关系数据库中(这就是 SQL,即房间覆盖),关系由 foreign keys 处理。您可以这样做的一种方法是在每个位置添加一个jobid,据我所知,您有一个一对多关系:一个Job 可以有多个@987654339 @。然后,房间将抓取来自Locationjobid 与来自Jobjobid 匹配的每个Location。建议先下载SQLite Studio之类的,先没有空间练习一下,这样你就能很好的理解了。
    猜你喜欢
    • 1970-01-01
    • 2019-07-25
    • 1970-01-01
    • 1970-01-01
    • 2022-07-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-03
    • 2016-05-10
    相关资源
    最近更新 更多