android - 尽管 autoGenerate=true,但主键的房间唯一约束失败

标签 android sqlite android-room primary-key

正如上面所说:我正在 Android 上使用 Room 2.4.1 来存储一些数据。我有一个实体,它设置为具有自动生成的主键。但是,我只能对该实体的实例执行一次插入(这会将主键字段设置为 0)。之后,应用程序崩溃,因为 SQLite 引发主键字段的唯一键违规。鉴于主键字段应该是自动生成的,这种情况不应该发生...我怎样才能阻止这种情况发生?当然,我可以自己管理增加 key ,但这违背了 Room 具有此功能的意义。

这是我的 Room 实体(为简单起见,删除了其他列)...

import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import android.location.Location;
import android.os.Build;

@Entity(tableName="foo")
public class Foo {
    @PrimaryKey(autoGenerate=true)
    private long id;

    public Foo() {

    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

它有一个关联的 DAO...

import androidx.room.Dao;
import androidx.room.Insert;

@Dao
public interface FooDao {
    @Insert
    long insert(Foo foo);
}

我尝试插入(请注意,我可以成功执行此操作一次,但如果我尝试生成插入第二个 Foo,则会出现错误)...

Foo foo = new Foo();
long fooId = fooDao.insert(foo);

我得到以下堆栈跟踪...

2022-01-28 14:28:01.027 15233-15278/com.bar.baz E/AndroidRuntime: FATAL EXCEPTION: StateController
    Process: com.bar.baz, PID: 15233
    android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: foo.id (code 1555)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:783)
        at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
        at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
        at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(SourceFile:51)
        at androidx.room.EntityInsertionAdapter.insertAndReturnId(SourceFile:114)
        at com.bar.baz.database.FooDao_Impl.insert(SourceFile:89)
        at ...

最佳答案

您的问题是,由于原始 long 默认为 0,因此 id 被设置为 0 和 UNIQUE 冲突(请参阅下面的更全面的解释,了解为什么这似乎矛盾文档)

我建议使用:-

@Entity(tableName="foo")
public class Foo {
    @PrimaryKey
    private Long id=null;

    public Foo() {

    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

Long 与 long 不同,它没有默认值,它是 null。 Room 看到空/主键组合并跳过添加值,以便生成值。

这样做的优点是不使用低效的自动增量(使用 Room 的 autogenerate=true 打开)。 id 列仍将生成,它是唯一的,通常比表中存在的最高 id 大 1。

同时,autogenerate = true (AUTOINCRMENT) 添加了一个附加规则,该规则是自动生成的 rowid 编号必须高于上次使用的编号。为了启用此功能,使用表 sqlite_sequence(为 AUTOINCRMENT 的第一个实例自动创建),该表存储最后分配的 rowid 值。

  • rowid 是一个隐藏列,至少对于 Room 表而言,它始终存在。当您有一个整数类型(boolean -> long 基元或对象)并且该列是主键时,那么该列就是 rowid 的别名。

因此,当使用 autogenerate = true 时,搜索和维护这个额外的表都会产生开销。

参见SQLite Autoincrement第一行说明了一切:-

The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed.

附加

回复评论:-

Thanks for the answer... the Room docs specifically state that for primary keys, if the ID field is initialized to 0, as it will be for an uninitialized Java primitive like long, the ID will be treated as "empty" and the auto-generated value will be applied... So, long vs Long shouldn't be the issue. Nevertheless, I tried switching to Long and leaving it uninitialized (i.e., null). Now Room throws a null pointer exception for Attempt to invoke virtual method 'long java.lang.Long.longValue()' on a null object reference. However, the docs say a null Long is acceptable too. I'm at a loss...

这是一个显示上面内容的工作示例。它使用 3 个类,原始类,修复(使用 Long 而不是带有 autogenerate=true 的 long),以及建议使用不带 autogenerate = true 的 Long 来提高效率。

类(class)是:-

@Entity(tableName="foo")
public class Foo {
    @PrimaryKey(autoGenerate = true)
    private long id;
    public Foo() {}
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

Foo1(修复)

@Entity(tableName="foo1")
public class Foo1 {
    @PrimaryKey(autoGenerate = true)
    private Long id;
    public Foo1() {}
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

Foo2(建议)

@Entity(tableName="foo2")
public class Foo2 {
    @PrimaryKey
    private Long id;
    public Foo2() {}
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

使用 @Dao 注释的类:-

@Insert(onConflict = IGNORE)
abstract long insert(Foo foo);
@Insert(onConflict = IGNORE)
abstract long insert(Foo1 foo1);
@Insert(onConflict = IGNORE)
abstract long insert(Foo2 foo2);
  • 使用抽象类而不是接口(interface)
  • 忽略,这样 UNIQUE 约束冲突就不会失败

最后是以下代码:-

    for(int i = 0; i < 3; i++) {
        dao.insert(new Foo());
        dao.insert(new Foo1());
        dao.insert(new Foo2());
    }

应用检查结果显示:-

sqlite_sequence(由应用检查隐藏,因此可通过查询访问)

enter image description here

因此,从这里可以看出 foo 表的最后一次插入是 0,foo1 的最后插入是 3,并且 foo2 表没有行(但也可以看出该表存在,忽略 MainTypeEntity 和来自另一个问题的 EmbeddedTypeEnity)。

Foo:-

enter image description here

可以看出,插入的第一行BUTid为0。这证明 Room 正在使用原语的值,即 0 并且不使用生成的值,因为(参见上面的链接 - 第 2 节 - 最后但 1 段)

If no ROWID is specified on the insert, or if the specified ROWID has a value of NULL, then an appropriate ROWID is created automatically. The usual algorithm is to give the newly created row a ROWID that is one larger than the largest ROWID in the table prior to the insert. If the table is initially empty, then a ROWID of 1 is used.

Foo1

enter image description here

可以看出,所有 3 行都已插入,并且第一个 id 是 1 而不是 0。从上面的 sqlite_sequence 表中,Foo1 使用 autoincrement 又名 autogenerate = true。

Foo2

enter image description here

文档确实说:-

If the field type is long or int (or its TypeConverter converts it to a long or int), Insert methods treat 0 as not-set while inserting the item.

然而,这不是全部真相,也不是全部真相。它没有继续详细说明何时不适用这一点。如果您查看构建日志,您会看到如下警告:-

warning: ... .Foo's id field has type long but its getter returns java.lang.Long. This mismatch might cause unexpected id values in the database when a.a.so70867141jsonstore.Foo is inserted into database.
private long id;

因此 getter(或 setter)可以否决。

现在将 Foo 更改为

@Entity(tableName="foo")
public class Foo {
    @PrimaryKey(autoGenerate = true)
    public long id;
    public Foo() {}
    public Long getId() {
        return id;
    }
    @Ignore //<<<<<<<<<< (plus id being public)
    public void setId(Long id) {
        this.id = id;
    }
}

或:-

@Entity(tableName="foo")
public class Foo {
    @PrimaryKey(autoGenerate = true)
    public long id;
    public Foo() {}
    @Ignore //<<<<<<<<<<
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

或者两者都@Ignore,那么 Foo 就可以工作并且 id 是自动生成的。

关于android - 尽管 autoGenerate=true,但主键的房间唯一约束失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70899422/

相关文章:

android - android中的依赖项是什么?

android - 如何将所有 JSON 记录存储到 SQLite 中

gcc - 在 Cygwin64 : "ld: cannot find -lmingw32" 上编译 cgo lib

android - 房间数据库 : What is Index specific columns (indices and @Index) and how to use it?

android - 无法添加依赖mapBox SDK错误 fragment

java - DefaultHttpClient GET 和 POST 命令 Java Android

java - Android 应用程序无异常退出

visual-studio-2010 - 安装后报告缺少 System.Data.SQLite.dll

java - Android Studio 中突然出现编译错误;错误 : cannot find symbol class ComputableLiveData

android - android中的房间数据库无法解析 'android.arch.persistence.room:runtime:1.0.0'