【问题标题】:Android Room FOREIGN KEY constraint failedAndroid Room FOREIGN KEY 约束失败
【发布时间】:2017-06-19 14:39:28
【问题描述】:

我正在尝试借助 Google 在 I/O 2017 中引入的 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;
}

在它自己的 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)

有人知道我的项目表有什么问题或建议另一种设计吗?

【问题讨论】:

  • 您没有指定父文件夹,我猜这就是触发此错误的原因。当然,您不能为第一个文件夹指定父文件夹,因为没有可指向的父文件夹。 Self-referential foreign keys work in SQLite,虽然它看起来像 you need to explicitly use NULL for the value when it is missing。我不知道 Room 为这里的 INSERT 语句生成了什么。
  • @CommonsWare 我试过 NULL 值,我也为父列输入了一个整数,例如 0,但结果没有改变,仍然出现这个错误。
  • @HamidrezaSahraei:请看一下这个。 stackoverflow.com/q/44749812/1788806 使用 String 作为数据类型对于您的用例来说是不必要的。您需要使用 Integer,因为这是一个可以为 null 的类(不是原始类型)。
  • 您是否在未进行全新安装的情况下更新了数据库模型?您的清单中还启用了自动备份吗??

标签: android sqlite orm android-room android-orm


【解决方案1】:

我得到了这个工作,但没有使用 int 主键。我不是那种 ORM 场景的超级粉丝,只是因为这些问题。

所以,这是一个自引用的 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);

【讨论】:

  • 这太好了,我用你的代码来实现文件夹结构,现在它的工作就像一个魅力。但是你知道我的代码有什么问题吗?
  • @HamidrezaSahraei:唯一显着的区别是您使用的是自动生成的 int 键,而我不是。您可以尝试切换到Integer,看看null 默认值是否效果更好。我的猜测是 0 默认 int 打破了外键约束,因为 Room 无法判断 0 表示“没有关系”。但是,这只是一个猜测。
  • 你可以让你的外键类型为整数,它可以为空。并保持您的 id 类型为 int
  • @Yushi:是的,截至1.0.0-alpha8 左右。在我撰写此答案时,该功能不存在。
  • @CommonsWare 我在更新时遇到问题,它不会抛出任何异常。但表格没有更新。假设您需要将项目从一个文件夹移动到另一个文件夹,基本上,您正在更改父级。如果你这样做,它就不起作用。也不会看到任何异常。这是自引用表的问题吗?请帮忙!!如何解决这个问题
【解决方案2】:

我花了很长时间才弄清楚,似乎根本不允许使用自动生成的主键 parentColumns 元素,即使它是使用 Kotlin 数据类的 uuid,默认值如下这个:

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

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

【讨论】:

  • 好像在使用autoGenerate = true时,room使用rowId列作为主键。但是 sqlite 不允许在 rowId 上使用 FOREIGN KEY。 FOREIGN KEY 必须以命名列为目标。 sqlite.org/foreignkeys.html 这可以解释为什么你不能有一个外键来定位自动生成的列。
  • @JDenais 当然你可以使用自动生成的列作为外键。您只是不能直接使用 rowid 作为键。
  • @Daniel Wilson 您首先订购 autoGenerate 然后自己设置值吗?对我来说没有任何意义。 developer.android.com/reference/android/arch/persistence/room/…
猜你喜欢
  • 1970-01-01
  • 2020-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多