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

Room unique constraint failed for primary key despite autoGenerate=true

几乎如罐头上所说:我在 Android 上使用 Room 2.4.1 来存储一些数据。我有一个设置为具有自动生成的主键的实体。但是,我只能插入该实体的一个实例(将主键字段设置为 0)。之后,应用程序崩溃,因为 SQLite 抛出主键字段的唯一键违规。这不应该发生,因为主键字段应该是自动生成的......我怎样才能阻止这种情况发生?当然,我可以自己管理递增密钥,但这违背了 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 看到 null/primary 组合键并跳过添加值,因此生成了值。

这样做的好处是不使用低效的 AUTOINCREMENT(使用 Room 的 autogenerate=true 会打开)。 id 列仍将生成,是唯一的,通常比 table.

中存在的最高 id 大 1

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

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

因此,当使用 autogenerate = true 时,搜索和维护这个额外的 table.

都会产生开销

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 个 classes,原来的,一个修复 (使用 Long 而不是 long with autogenerate=true) 并且建议更有效地使用 Long without autogenerate = true.

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 注释的 class :-

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

最后是以下代码:-

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

App 检查结果显示:-

sqlite_sequence(被 App Inspection 隐藏,通过查询访问)

所以从这里可以看出 foo table 的最后插入是 0 而 foo1 的最后插入是 3 并且 foo2 table 没有行(但是作为也可以看出table存在,忽略另一个问题的MainTypeEntity和EmbeddedTypeEnitity)。

:-

可以看出第一行被插入 BUTid 为 0。这证明了 Room正在使用原语的值,即 0 而不是使用生成的值,因为(请参阅上面的 link - 第 2 节 - 最后一段)

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

可以看出所有 3 行都已插入,第一个 id 是 1 而不是 0。从上面的 sqlite_sequence table Foo1 使用自动增量 aka autogenerate = true。

Foo2

文档确实说:-

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 的 then Foo 工作并且 id 是自动生成的。

这里的问题是意外不匹配:Foo class 中的主键 (id) 字段是 long,但 getter 对于 id (getId()) returns a Long.

The Room docs state the following about primary keys...

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. If the field's type is Integer or Long (or its TypeConverter converts it to an Integer or a Long), Insert methods treat null as not-set while inserting the item.

在导致问题的代码中,id 未初始化,因此取值为 0。如果 getter 返回 long,Room 会考虑它un-set 并且会有 auto-generated 一个 ID 值。但是,由于 getter returns a Long,Room 将 0 解释为每次插入时主键字段的预期值,从而导致违反唯一键约束。

Android Studio / Room do 当发生这种不匹配时会产生有用的警告,应该注意...

warning: com.company.project.database.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 com.company.project.database.Foo is inserted into database.

auto-generating 主键的解决方案是在字段中有一个原语 long,将该值设置为 0(或未初始化,将为 0),并且 有一个 getter,returns 有一个 long。或者,您可以在字段中设置 Long,将该值设置为 null(或未初始化,即 null)、有一个 getter 那 returns 一个 Long。您根本无法混合搭配。

有关 auto-generating Room 主键的其他性能建议,请参阅 MikeT 的回答。