Android Room 外键约束失败

15

我正在尝试使用Android Room Persistence (一种ORM),在Android SQLite中设计和实现文件夹树结构。在我的设计中,一个文件夹可以包含其他文件夹和文件。以下是我的文件夹和文件的代码:

文件模型:

@Entity(tableName = "files", foreignKeys = @ForeignKey(entity = Folder.class,
    parentColumns = "id",
    childColumns = "parent_id",
    onDelete = CASCADE))
public class File {
    @PrimaryKey(autoGenerate = true)
    private int id;

    private String title;
    private Date creationDate;

    @ColumnInfo(name = "parent_id")
    public int parentId;

    //here setters and getters skipped but exist in original code
}

这里是文件夹代码:

@Entity(tableName = "folders", foreignKeys = @ForeignKey(entity = Folder.class,
    parentColumns = "id",
    childColumns = "parent_id",
    onDelete = CASCADE,onUpdate = SET_NULL))
public class Folder {
    @PrimaryKey(autoGenerate = true)
    private int id;

    private String name;

    @ColumnInfo(name = "parent_id")
    private int parentId;
}

Folder表中有一个指向其自身父级的ID列的外键。

以下是FolderDAO:

@Dao
public interface FolderDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertFolder(Folder... folders);

    @Update
    public void updateFolder(Folder... folders);


    @Delete
    public void deleteFolders(Folder... folders);

    @Query("SELECT * FROM folders")
    List<Folder> getAll();

    @Query("SELECT * FROM folders WHERE id IN (:folderIds)")
    List<Folder> loadAllByIds(int[] folderIds);
}

但是当我创建一个文件夹对象并尝试插入它时:

AsyncTask.execute(new Runnable() {
        @Override
        public void run() {
            Folder folder = new Folder();
            folder.setName("All");

            DatabaseInstance.getInstance(getApplicationContext()).folderDAO().insertFolder(folder);

        }
    });

收到以下错误:

外键约束失败

         --------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #1
              Process: sahraei.hamidreza.com.note, PID: 1835
              android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787)
                  at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
                  at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:782)
                  at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
                  at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
                  at android.arch.persistence.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.java:80)
                  at android.arch.persistence.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.java:80)
                  at sahraei.hamidreza.com.note.DAO.FolderDAO_Impl.insertFolder(FolderDAO_Impl.java:80)
                  at sahraei.hamidreza.com.note.ItemListActivity$1.run(ItemListActivity.java:62)
                  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
                  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
                  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
                  at java.lang.Thread.run(Thread.java:818)

有人知道我的项目表有什么问题或者能提出另一个设计吗?


您尚未指定父文件夹,我猜这就是触发此错误的原因。当然,对于第一个文件夹,您无法指定父文件夹,因为没有父文件夹可以指向。SQLite中的自引用外键有效,尽管看起来在值缺失时需要显式使用NULL。我不知道 Room 在此处生成什么 INSERT 语句。 - CommonsWare
@CommonsWare 我已经尝试了NULL值,还为父列放入了一个整数,例如0,但结果没有改变,仍然出现这个错误。 - Hamidreza Sahraei
1
@HamidrezaSahraei:请看一下这个链接 https://stackoverflow.com/q/44749812/1788806。在您的用例中,使用字符串作为数据类型是不必要的。您需要使用整数,因为这是一个类(而不是原始类型),可以为空。 - Willi Mentzel
你更新了数据库模型但没有进行干净的安装吗?另外,你在清单中启用了自动备份吗? - Jaswanth Manigundan
2个回答

10

我让它工作了,但没有使用int主键。对于这种ORM场景,我不是很喜欢使用int主键,因为会出现这种问题。

所以,这里有一个自引用的Category类,它使用UUID作为主键:

/***
 Copyright (c) 2017 CommonsWare, LLC
 Licensed under the Apache License, Version 2.0 (the "License"); you may not
 use this file except in compliance with the License. You may obtain  a copy
 of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
 by applicable law or agreed to in writing, software distributed under the
 License is distributed on an "AS IS" BASIS,  WITHOUT WARRANTIES OR CONDITIONS
 OF ANY KIND, either express or implied. See the License for the specific
 language governing permissions and limitations under the License.
 */

package com.commonsware.android.room.dao;

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import java.util.UUID;
import static android.arch.persistence.room.ForeignKey.CASCADE;

@Entity(
  tableName="categories",
  foreignKeys=@ForeignKey(
    entity=Category.class,
    parentColumns="id",
    childColumns="parentId",
    onDelete=CASCADE),
  indices=@Index(value="parentId"))
public class Category {
  @PrimaryKey
  public final String id;
  public final String title;
  public final String parentId;

  @Ignore
  public Category(String title) {
    this(title, null);
  }

  @Ignore
  public Category(String title, String parentId) {
    this(UUID.randomUUID().toString(), title, parentId);
  }

  public Category(String id, String title, String parentId) {
    this.id=id;
    this.title=title;
    this.parentId=parentId;
  }
}

现在您可以拥有类似以下的DAO方法:

@Query("SELECT * FROM categories WHERE parentId IS NULL")
Category findRootCategory();

@Query("SELECT * FROM categories WHERE parentId=:parentId")
List<Category> findChildCategories(String parentId);

1
太棒了,我使用了你的代码来实现文件夹结构,现在它像魔法一样工作。但是你知道我的代码有什么问题吗? - Hamidreza Sahraei
3
唯一的显著区别是您正在使用自动生成的"int"键,而我没有。您可以尝试切换到"Integer"并查看是否使用"null"默认值更好。我的猜测是"default"为0的"int"破坏了外键约束,因为Room无法知道"0"的意思是"没有关系"。但这只是一个猜测。 - CommonsWare
1
您可以将外键类型设置为可空的整数(Integer),而保持id类型为int。 - Yushi
1
@Yushi:是的,从1.0.0-alpha8版本开始支持该功能。在我回答这个问题的时候还不存在该功能。 - CommonsWare
@CommonsWare 我在更新时遇到了问题,它不会抛出任何异常,但表格没有更新。假设您需要将项目从一个文件夹移动到另一个文件夹,基本上是更改父级。如果您这样做,它就无法工作。也不会看到任何异常。这是否与自引用表有关的问题?请帮忙解决!如何解决这个问题? - sreekumar
显示剩余2条评论

5

我花了很长时间才明白,即使使用一个带有默认值的Kotlin数据类,使用自动生成的主键作为parentColumns元素似乎是不允许的,即使它是一个uuid,也是如此。

class MyClass {
    // Don't do this
    @PrimaryKey(autoGenerate = true)
    var uuid: String = UUID.randomUUID()
}

始终确保您直接指定了父列。


4
使用 autoGenerate = true 时,Room 使用 rowId 列作为主键。但是 SQLite 不允许在 rowId 上使用 FOREIGN KEY, FOREIGN KEY 必须指向一个具名列。这就解释了为什么无法将 FOREIGN KEY 指向自动生成的列。参见:https://www.sqlite.org/foreignkeys.html。 - JDenais
@JDenais,当然可以使用自动生成的列作为外键。你只是不能直接使用rowid作为键。 - The incredible Jan
@Daniel Wilson 你先自动生成再手动设置值?这对我来说没有任何意义。https://developer.android.com/reference/android/arch/persistence/room/PrimaryKey - The incredible Jan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接