如何以干净的方式创建外键与 Room DB 相关的行和子项?

Posted

技术标签:

【中文标题】如何以干净的方式创建外键与 Room DB 相关的行和子项?【英文标题】:How can I create row and children related by ForeignKey with RoomDB in a clean way? 【发布时间】:2021-11-25 11:48:51 【问题描述】:

这个问题在某种程度上与my last question有关,因为它是同一个项目,但现在我正努力向前迈出一步。

所以,在我之前的问题中,我只有一张桌子;这次我有两个表:新的第二个表应该包含第一个表的行的相关属性,在 OneToMany 关系中。因此,我在第二个表中存储了一个 ForeignKey,它将存储第一个表的相关行的 Row ID(显然)。

问题是这样的:目的是同时创建两个寄存器(父和子),使用相同的形式,而 ParentTable 使用 AUTO_INCREMENT 作为他的 PrimaryKey(AKA ID)。 p>

由于 RoomDb 的工作方式,我使用 POJO 进行创建:但在插入后,据我所知,此 POJO 不会给我自动生成的 ID...所以,我能做到的唯一解决方法想象一下,在提交表单时,首先为父级创建 INSERT,然后使用创建父级的表单字段之一进行某种“SELECT * FROM parent_table WHERE field1 LIKE :field1”,检索 ID,然后使用该 ID 创建子表的 POJO 并执行下一个 INSERT 操作。但是我觉得这种方法有些不对劲,上次我以这种方式实现类似的东西时,我最终得到了很多自定义侦听器和回调地狱(我仍然对此做噩梦)。

关于自定义侦听器这件事,这是我最终为不同项目选择的不同问题的解决方案(有关此问题的更多详细信息,请参见old question)。看看那个老问题可能有助于添加一些关于我在 MVVM 架构中被误导的背景。但是,请注意当前的问题与 WebServices 无关,因为数据库纯粹是手机应用程序中的本地,没有外部来源。

但是,我想知道:这不是矫枉过正吗(我的意思是INSERT parent -> SELECT parentID -> INSERT child 的东西)?这样做是不可避免的,还是有更干净的方法?


我的 Repository 类中的“创建方法”如下所示:

public void insertParent(Parent parent) 
    new InsertParentAsyncTask(parent_dao).execute(parent);


private static class InsertParentAsyncTask extends AsyncTask<Parent, Void, Void> 
    private final ParentDao parent_dao;
    private InsertParentAsyncTask(ParentDao parent_dao) 
        this.parent_dao = parent_dao;
    

    @Override
    protected Void doInBackground(Parent... parents) 
        parent_dao.insert(parents[0]);
        return null;
    

试图按照马里奥的回答,我在我父母的 DAO 中改变了这个方法:

// OLD
@Insert
void insert(Parent parent);

// NEW (yes, I use short type for parent's ID)
@Insert
short insert(Parent parent);

EDIT2: 现在,我正在尝试对我的存储库的插入 AsyncTask 进行更改,如下所示:

private static class InsertParentAsyncTask extends AsyncTask<Parent, Void, Short> 
    private final ParentDao parent_dao;
    private InsertParentAsyncTask(ParentDao parent_dao) 
        this.parent_dao = parent_dao;
    

    @Override
    protected Short doInBackground(Parent... parents) 
        short parent_id;
        parent_id = parent_dao.insert(parents[0]);
        return parent_id;
    

    @Override
    protected void onPostExecute(Short hanzi_id) 
        // TODO ??? What now?
    

长话短说

它在这里对我有用,但这不是干净的代码(显然):

// TODO I am aware that AsyncTask is deprecated
// My Repository class uses this
public void insertParentAndChildren(Parent parent, String[] children_list) 
    new InsertParentAndChildrenAsyncTask(parent_dao, children_list).execute(parent);


private static class InsertParentAndChildrenAsyncTask extends AsyncTask<Parent, Void, Short> 
    private final ParentDao parent_dao;
    private String[] children_list;
    private InsertParentAndChildrenAsyncTask(ParentDao parent_dao, String[] children_list) 
        this.parent_dao = parent_dao;
        this.children_list = children_list;
    

    @Override
    protected Short doInBackground(Parent... parents) 
        short parent_id;
        Long row_id = parent_dao.insert(parents[0]);
        parent_id = parent_dao.getIdForRowId(row_id);
        return parent_id;
    

    @Override
    protected void onPostExecute(Short parent_id) 
        // Second "create method" for children
        for (int n = 0; n < children_list.length; n++) 
            Child child = new Child();
            child.setParentId( parent_id );
            child.setMeaning( children_list[n] );
            // My Repository has this method as well
            insertChildStaticMethod(child);
        
    

【问题讨论】:

【参考方案1】:

你在正确的轨道上。一种干净的方法是将其包装在这样的函数中:

fun saveParent(parent: Parent): Int 
    val rowId = parentDao.insert(parent) // Returns Long rowId
    val parentId = parentDao.getIdForRowId(rowId) // SELECT id FROM table_parent WHERE rowid = :rowId
    return parentId

这个函数可以成为存储库类的一部分,使其更加干净。 您在 DAO 中的函数可以像这样返回 rowId 和 Parent.ID:

@Insert
fun insert(parent: Parent): Long

@Query("SELECT ID FROM table_parent WHERE rowid = :rowId")
fun getIdForRowId(rowId: Long): short

如果您想首先使用基本功能,您可以在使用allowMainThreadQueries() 构建数据库时在主线程上调用 Room 数据库函数:

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()

像这样,您可以将后台处理推迟到以后。如果您对该主题有具体问题,最好单独提出问题。

【讨论】:

哦,Kotlin(太可爱了,但我仍然使用 Java :P)。无论如何,我想看看你的答案,DAO 的创建方法能够返回一个 ID?很高兴知道...我必须尝试一下 既然你喜欢“干净”,那就去 Kotlin 吧!你永远不会回头。 :) 查看我的编辑以从 insert 方法中获取 rowId。 我猜Kotlin更简洁毫无疑问......但是这只老狗(我是说我)仍然害怕无法适应Kotlin(同样的借口拖延学习Kotlin 2年已经)......无论如何你有一点,Kotlin 的样板代码比 Java 少,这是肯定的...... 房间插入不返回父id/主键而是rowid,查看这里:developer.android.com/training/data-storage/room/accessing-data。 对于基本功能,可以考虑使用 Room allowMainThreadQueries(),因此不需要 asyncTask 或协程。然后您可以稍后决定如何获取主线程的数据库访问功能。不,Java 中不存在协程/挂起函数。相反,Java 有“回调地狱”;)。【参考方案2】:

我认为您可以尝试SELECT last_insert_rowid() 作为房间查询(您可以这样编写,无需引用任何表)。此语句返回上次插入数据库的 rowid。默认情况下,当您将 rowId 定义为自动增量整数时,大多数 sql DB 将它们用作主键。所以我猜你会在你的 DAO 中定义以下方法

@Query("SELECT last_insert_rowid()")
public abstract int getLastId()

@Insert
void insert(Parent parent)

然后您可以在事务中将它与您的插入语句一起使用。像这样

@Transaction
public int insertAndGetPrimaryKey(Parent parent)
    insert(parent);
    return getLastId();

使用事务很重要,否则如果在您的应用中多个线程可能同时修改表,则传递的 id 可能是错误的。

顺便说一句,我不会使用短作为主键。不仅 XD 短(只有 32k 容量),而且还真的要使用 Integer(40 亿容量)。如果这些是 80 年代的 id 全部用于它(那些是你在所有 XD 之后保存的 2 个完整字节)但现在一天的内存既便宜又丰富,正如我所说的整数是 DB 默认使用的。

【讨论】:

令人印象深刻!单线程和多线程 Android Java 代码的干净解决方案!使用 DB 默认的主键也是清理代码问题的一个好点。 乐于助人!! 这对你有用吗?有一个替代方法,您可以按 rowId 以 desc 顺序对父表进行排序,并限制到第一行。但它在功能上应该是完全相同的代码,您仍然希望使用该事务。唯一的区别是它允许您使用任何其他类型作为主键(在我看来这并不是那么有用) ...我想就此向您提供反馈,但遗憾的是我还没有时间重构。我几乎不能每天花一两个小时来编码(我喜欢编码的日子,因为我每周工作 9,5 小时每周 6 天(本周 7 天 T_T)......现在,我的编码时间去了除了重构之外的任何事情...... T_T 但是,我知道数据库事务及其用途;因此,即使您的代码尚未经过测试,我也知道它会起作用。

以上是关于如何以干净的方式创建外键与 Room DB 相关的行和子项?的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server:主键与外键设置与相关理解

在navicat中怎么外键与主键关联

以编程方式导入 Room 数据库

sql用命令创建主键与外键。

sql server 主键与外键约束无法创建

如何分清SQL数据库中的主键与外键