android 为什么@PrimaryKey瓦尔id:Int?= null在创建房间实体时是否有效?

ss2ws0br  于 2023-04-18  发布在  Android
关注(0)|答案(2)|浏览(153)

我正在学习如何使用Jetpack Compose创建笔记应用程序的教程。这是tutorial的链接。教程中有一点他创建了这个实体:

@Entity
data class Note(
    val title: String,
    val content: String,
    val timestamp: Long,
    val color: Int,
    @PrimaryKey val id: Int? = null
)

请注意,他使用的是一个可空值作为主键,而不是(autoGenerate = true)
我认为这是行不通的。首先因为主键不应该为null,其次因为没有autoGenerate,id将如何表现?它们不会都有相同的id吗?
我在创建房间实体时经常看到的代码是这样的:

@PrimaryKey(autoGenerate = true) val id: Int = 0

@PrimaryKey注解是如何工作的?为什么两种方式都有效?哪一种更值得推荐?
编辑:我想澄清一下,教程中使用的代码可以正确编译并保存到数据库中。

xlpyo6sf

xlpyo6sf1#

我认为这是行不通的,首先因为主键不应该为null,其次因为没有autoGenerate,id将如何表现?
Room实际上使用null,如果autogenerate为true,则将0转换为null,因为SQLite在INTEGER PRIMARY KEY的特殊情况下,当指定null时会生成一个值。

  • 由于SQLite要求INTEGER PRIMARY KEY是一个整数,因此它处理null,在这种特殊情况下,生成整数值。

rowid表的PRIMARY KEY约束(只要它不是真正的主键或INTEGER PRIMARY KEY)实际上与UNIQUE约束是一样的。因为它不是真正的主键,所以PRIMARY KEY的列允许为NULL,这违反了所有SQL标准。

如果autogenerate为true,则生成的代码(根据/从下面的演示)包括:

"INSERT OR IGNORE INTO `AutoGenTrueTable` (`id`,`name`) VALUES (nullif(?, 0),?)"

而在autogenerate为false的情况下,生成的代码使用:

"INSERT OR IGNORE INTO `AutoGenFalseTable` (`id`,`name`) VALUES (?,?)"
  • 注意,OR IGNORE是因为IGNORE的onConflictStrategy。
  • 如果id字段为0,则后一个示例(即autogenerate为假)将因此使用值0
  • 生成的java可以通过Android Studio的Android View轻松找到。DAO位于与接口/抽象类同名但后缀为_Impl的类中。
  • 通过Room使用的Android SQLite API的绑定将null转换为null关键字(令牌)。
  • @Database注解类同名但后缀为_Impl的类将具有其他有用的代码,例如在createAllTables方法中,可以找到用于创建表的SQL。

他们不是都有相同的身份证吗?

NO作为主键是隐式唯一的,因此如果它是主键,并且不管自动生成的是true还是false,则同一个表中的id永远不会与另一个id相同。

如果**autogenerate为true**,则Room还将0转换为不提供值,因此0会导致生成值。

但是如果你指定一个值0,如果autogenerate为false*(默认情况下显式或隐式)* 那么0将被用于id,这将不允许超过一次,但可以由插入的onConflictStrategy处理。

下面的DEMO说明了上述 (注意,使用了IGNORE onConflictStrategy,因此忽略了错误的重复0 id)

一点关于INTEGER PRIMARY KEY(例如@PrimaryKey val whatever:Int或更正确的Long)的信息,也就是rowid列的别名。

  • 可以使用Byte、Short等,因为它们是整数类型,但它们的用途有限。
  • Long更正确,因为值可以大到64位有符号整数(Int不够大,Long足够大,在许多情况下这不是问题)。

如果一个列是INTEGER (Room在编译时确定) 并且是PRIMARY KEY(在列或表级别),那么该列是一个特殊的通常隐藏的列的别名,rowid,所有表都有(除了WITHOUT ROWID表,Room不支持通过注解)。

  • 注意,rowid列始终存在(除非该表是WITHOUT ROWID表,同样,Room不支持该表),并且始终被分配一个整数值(无论是否指定或暗示INTEGER PRIMARY KEY)。
  • 尽管使用了rowid,SQLite也接受其他别名,有关rowid的更多信息,请参见https://www.sqlite.org/rowidtable.html

这样的列必须是INTEGER类型的值 (除了rowid列或其别名之外,任何列类型实际上都可以存储任何类型的值,尽管Room不支持这一点)。此外,如果在插入时没有为这样的列指定值,则该值将由SQLite生成。这通常比该表的最大rowid大1。
因此,只要情况是没有为列提供值,则将生成该值(并且很可能比最高值大1)。
如果使用Room的autogenerate=true,则会将SQLite AUTOINCREMNET关键字添加到表定义/模式中。这会更改值的生成方式,因为它是两个值中的较大值,一个是表中最高的rowid,另一个是记录/使用过的最高rowid值,可能高于最高的rowid,如果具有最高rowid的行已被删除。

  • 注意,这里假设sqlite_sequence表没有被改变,除了SQLite对表的处理 (它可以被操纵但要小心

简而言之,AUTOINCREMENT添加了一个约束/规则,规定生成的值必须大于任何使用的值。然而,这要求最高分配值必须存储在其他地方。SQLite将此附加值存储在名为sqlite_sequence的表中,每个表将有1行。获取和维护这样的值会产生开销,因此SQLite文档声明:-

    • AUTOINCREMENT关键字会带来额外的CPU、内存、磁盘空间和磁盘I/O开销,如果不是绝对需要,应该避免使用。通常不需要。* 请参阅https://www.sqlite.org/autoinc.html

使用null或0与autogenerate=true相同,Room不提供值,因此生成值。如果提供任何其他值,则使用该值(如果行已经存在,则会导致唯一冲突,插入或更新时,使用相应注解的onConFlictStrategy参数('@ Insert'或@Update)进行唯一冲突处理

  • 如果使用INSERT或UPDATE查询,则可以指定实际的SQLite OR????on conflict action,例如'INSERT OR IGNORE...'

如前所述,如果没有autogenerate=true,0将被用作值(参见下面的演示),因此,为了避免AUTOINCREMENT的开销/浪费/效率低下,该字段应该是可空的,并使用null来生成值。

*Java,其中primitives(int,long)的默认值为0并且不能为null,是/是有点不同,有一些陷阱。
DEMO

也许可以考虑下面的演示,其中使用了3个表(实体),其中一个使用了autogenerate=true,另外2个没有指定,因此autogenerate=false是隐含的。另外两个之间的区别是,第一个不允许id为null,默认id为0,第二个允许id的默认值为null。
3个@Entity注解类是:

@Entity
data class AutoGenTrueTable(
    @PrimaryKey(autoGenerate = true)
    val id: Long=0,
    val name: String
)
@Entity
data class AutoGenFalseTable(
    @PrimaryKey
    val id: Long=0,
    val name: String
)
@Entity
data class AutoGenFalseNullableTable(
    @PrimaryKey
    val id: Long?=null,
    val name: String
)

要演示sqlite_sequence(从中提取所有数据),请执行POJO:-

data class SQLiteSequence(
    val name: String,
    val seq: Long
)

一个@Dao注解接口(用于插入、专门删除和提取数据):-

@Dao
interface AllDAOs {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(autoGenTrueTable: AutoGenTrueTable): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(autoGenFalseTable: AutoGenFalseTable): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(autoGenFalseNullableTable: AutoGenFalseNullableTable): Long

    @Query("DELETE FROM autogentruetable WHERE id >=:highAsOrHigherThanId")
    fun deleteFromAGTTByHighIds(highAsOrHigherThanId: Long)
    @Query("DELETE FROM autogenfalsetable WHERE id >=:highAsOrHigherThanId")
    fun deleteFromAGFTByHighIds(highAsOrHigherThanId: Long)
    @Query("DELETE FROM autogenfalsenullabletable WHERE id >=:highAsOrHigherThanId")
    fun deleteFromFalseNullableByHighIds(highAsOrHigherThanId: Long)

    @Query("SELECT * FROM autogentruetable")
    fun getAllFromAutoGenTrue(): List<AutoGenTrueTable>
    @Query("SELECT * FROM autogenfalsetable")
    fun getAllFromAutoGenFalse(): List<AutoGenFalseTable>
    @Query("SELECT * FROM autogenfalsenullabletable")
    fun getAllFromAutoGenFalseNullable(): List<AutoGenFalseNullableTable>
    @Query("SELECT * FROM sqlite_sequence")
    fun getAllFromSQLiteSequence(): List<SQLiteSequence>
}

一个非常直接的@Database注解的抽象类,允许使用主线程来简化演示:

@Database(entities = [AutoGenTrueTable::class,AutoGenFalseTable::class,AutoGenFalseNullableTable::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDAOs(): AllDAOs

    companion object {
        private var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context, TheDatabase::class.java, "the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}

最后是一些插入/提取和删除数据的活动代码,对于所有3个演示表,在各个阶段将提取的数据(包括sqlite_master的内容)写入日志:

class MainActivity : AppCompatActivity() {

    lateinit var db: TheDatabase
    lateinit var dao: AllDAOs
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getInstance(this)
        dao = db.getAllDAOs()
        var stage = 0
        logEverything(stage++)

        for (i: Int in 1..3) {
            dao.insert(AutoGenTrueTable(0,"AG_TT_ZERO_${i}"))
            //dao.insert(AutoGenTrueTable(null,"AG_TT_NULL_${i}")) not nullable cannot be used
            dao.insert(AutoGenTrueTable(name = "AG_TT_DEFAULT_${i}"))
            dao.insert(AutoGenTrueTable(id = 100,"AG_TT_100_${i}"))

            dao.insert(AutoGenFalseTable(0,"AG_FT_ZERO_${i}"))
            //dao.insert(AutoGenFalseTable(id = null,name = "AG_FT_NULL_${i}")) not nullable cannot be used
            dao.insert(AutoGenFalseTable(name = "AG_FT_DEFAULT_${i}"))
            dao.insert(AutoGenFalseTable(id = 100, "AG_FT_100_${i}"))

            dao.insert((AutoGenFalseNullableTable(0, "AG_FTNULL_ZERO_${i}") ))
            dao.insert((AutoGenFalseNullableTable(null, "AG_FTNULL_NULL_${i}") ))
            dao.insert((AutoGenFalseNullableTable( name = "AG_FTNULL_DEFAULT_${i}") ))
            dao.insert(AutoGenFalseNullableTable(id = 100, name = "AG_FTNULL_100_${i}"))
        }
        logEverything(stage++)
        
        
        dao.deleteFromAGTTByHighIds(100)
        dao.deleteFromAGFTByHighIds(100)
        dao.deleteFromFalseNullableByHighIds(100)

        logEverything(stage++)
        
        
        for (i: Int in 1..3) {
            dao.insert(AutoGenTrueTable(0,"AG_TT_ZERO_${i}"))
            //dao.insert(AutoGenTrueTable(null,"AG_TT_NULL_${i}")) not nullable cannot be used
            dao.insert(AutoGenTrueTable(name = "AG_TT_DEFAULT_${i}"))
            dao.insert(AutoGenTrueTable(id = 100,"AG_TT_100_${i}"))

            dao.insert(AutoGenFalseTable(0,"AG_FT_ZERO_${i}"))
            //dao.insert(AutoGenFalseTable(id = null,name = "AG_FT_NULL_${i}")) not nullable cannot be used
            dao.insert(AutoGenFalseTable(name = "AG_FT_DEFAULT_${i}"))
            dao.insert(AutoGenFalseTable(id = 100, "AG_FT_100_${i}"))

            dao.insert((AutoGenFalseNullableTable(0, "AG_FTNULL_ZERO_${i}") ))
            dao.insert((AutoGenFalseNullableTable(null, "AG_FTNULL_NULL_${i}") ))
            dao.insert((AutoGenFalseNullableTable( name = "AG_FTNULL_DEFAULT_${i}") ))
            dao.insert(AutoGenFalseNullableTable(id = 100, name = "AG_FTNULL_100_${i}"))
        }
        logEverything(stage++)
    }

    fun logEverything(stage: Int) {
        Log.d("DBINFO_STARTSTAGE_${stage}","Starting logging of stage ${stage}")
        logAllFromAGTT(stage)
        logAllFromAGFT(stage)
        logAllFromAGFTN(stage)
        logAllFromSQLite_Sequence(stage)
    }

    fun logAllFromAGTT(stage: Int) {
        for(a in dao.getAllFromAutoGenTrue()) {
            Log.d("DBINFO_AGTT_STG${stage}","ID is ${a.id} NAME is ${a.name}")
        }
    }
    fun logAllFromAGFT(stage: Int) {
        for(a in dao.getAllFromAutoGenFalse()) {
            Log.d("DBINFO_AGFT_STG${stage}","ID is ${a.id} NAME is ${a.name}")
        }
    }

    fun logAllFromAGFTN(stage: Int) {
        for(a in dao.getAllFromAutoGenFalseNullable()) {
            Log.d("DBINFO_AGFTN_STG${stage}","ID is ${a.id} NAME is ${a.name}")
        }
    }

    fun logAllFromSQLite_Sequence(stage: Int) {
        for(ss in dao.getAllFromSQLiteSequence()) {
            Log.d("DBINFO_SSEQ_STG${stage}","TABLE IS ${ss.name} HIGHEST ID STORED FOR THE TABLE IS ${ss.seq}")
        }
    }
}

当第一次运行应用程序安装时,输出为(阶段之间的2个空行和3个表之间的空行):-

2023-04-14 12:01:26.073  D/DBINFO_STARTSTAGE_0: Starting logging of stage 0

2023-04-14 12:01:26.244  D/DBINFO_STARTSTAGE_1: Starting logging of stage 1
2023-04-14 12:01:26.246  D/DBINFO_AGTT_STG1: ID is 1 NAME is AG_TT_ZERO_1
2023-04-14 12:01:26.246  D/DBINFO_AGTT_STG1: ID is 2 NAME is AG_TT_DEFAULT_1
2023-04-14 12:01:26.246  D/DBINFO_AGTT_STG1: ID is 100 NAME is AG_TT_100_1
2023-04-14 12:01:26.246  D/DBINFO_AGTT_STG1: ID is 101 NAME is AG_TT_ZERO_2
2023-04-14 12:01:26.246  D/DBINFO_AGTT_STG1: ID is 102 NAME is AG_TT_DEFAULT_2
2023-04-14 12:01:26.247  D/DBINFO_AGTT_STG1: ID is 103 NAME is AG_TT_ZERO_3
2023-04-14 12:01:26.247  D/DBINFO_AGTT_STG1: ID is 104 NAME is AG_TT_DEFAULT_3

2023-04-14 12:01:26.249  D/DBINFO_AGFT_STG1: ID is 0 NAME is AG_FT_ZERO_1
2023-04-14 12:01:26.249  D/DBINFO_AGFT_STG1: ID is 100 NAME is AG_FT_100_1

2023-04-14 12:01:26.250  D/DBINFO_AGFTN_STG1: ID is 0 NAME is AG_FTNULL_ZERO_1
2023-04-14 12:01:26.250  D/DBINFO_AGFTN_STG1: ID is 1 NAME is AG_FTNULL_NULL_1
2023-04-14 12:01:26.250  D/DBINFO_AGFTN_STG1: ID is 2 NAME is AG_FTNULL_DEFAULT_1
2023-04-14 12:01:26.250  D/DBINFO_AGFTN_STG1: ID is 100 NAME is AG_FTNULL_100_1
2023-04-14 12:01:26.251  D/DBINFO_AGFTN_STG1: ID is 101 NAME is AG_FTNULL_NULL_2
2023-04-14 12:01:26.251  D/DBINFO_AGFTN_STG1: ID is 102 NAME is AG_FTNULL_DEFAULT_2
2023-04-14 12:01:26.251  D/DBINFO_AGFTN_STG1: ID is 103 NAME is AG_FTNULL_NULL_3
2023-04-14 12:01:26.251  D/DBINFO_AGFTN_STG1: ID is 104 NAME is AG_FTNULL_DEFAULT_3
2023-04-14 12:01:26.253  D/DBINFO_SSEQ_STG1: TABLE IS AutoGenTrueTable HIGHEST ID STORED FOR THE TABLE IS 104

2023-04-14 12:01:26.258  D/DBINFO_STARTSTAGE_2: Starting logging of stage 2
2023-04-14 12:01:26.261  D/DBINFO_AGTT_STG2: ID is 1 NAME is AG_TT_ZERO_1
2023-04-14 12:01:26.261  D/DBINFO_AGTT_STG2: ID is 2 NAME is AG_TT_DEFAULT_1

2023-04-14 12:01:26.262  D/DBINFO_AGFT_STG2: ID is 0 NAME is AG_FT_ZERO_1

2023-04-14 12:01:26.263  D/DBINFO_AGFTN_STG2: ID is 0 NAME is AG_FTNULL_ZERO_1
2023-04-14 12:01:26.263  D/DBINFO_AGFTN_STG2: ID is 1 NAME is AG_FTNULL_NULL_1
2023-04-14 12:01:26.263  D/DBINFO_AGFTN_STG2: ID is 2 NAME is AG_FTNULL_DEFAULT_1
2023-04-14 12:01:26.264  D/DBINFO_SSEQ_STG2: TABLE IS AutoGenTrueTable HIGHEST ID STORED FOR THE TABLE IS 104

2023-04-14 12:01:26.333  D/DBINFO_STARTSTAGE_3: Starting logging of stage 3

2023-04-14 12:01:26.336  D/DBINFO_AGTT_STG3: ID is 1 NAME is AG_TT_ZERO_1
2023-04-14 12:01:26.336  D/DBINFO_AGTT_STG3: ID is 2 NAME is AG_TT_DEFAULT_1
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 100 NAME is AG_TT_100_1
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 105 NAME is AG_TT_ZERO_1
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 106 NAME is AG_TT_DEFAULT_1
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 107 NAME is AG_TT_ZERO_2
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 108 NAME is AG_TT_DEFAULT_2
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 109 NAME is AG_TT_ZERO_3
2023-04-14 12:01:26.337  D/DBINFO_AGTT_STG3: ID is 110 NAME is AG_TT_DEFAULT_3

2023-04-14 12:01:26.340  D/DBINFO_AGFT_STG3: ID is 0 NAME is AG_FT_ZERO_1
2023-04-14 12:01:26.340  D/DBINFO_AGFT_STG3: ID is 100 NAME is AG_FT_100_1

2023-04-14 12:01:26.342  D/DBINFO_AGFTN_STG3: ID is 0 NAME is AG_FTNULL_ZERO_1
2023-04-14 12:01:26.342  D/DBINFO_AGFTN_STG3: ID is 1 NAME is AG_FTNULL_NULL_1
2023-04-14 12:01:26.342  D/DBINFO_AGFTN_STG3: ID is 2 NAME is AG_FTNULL_DEFAULT_1
2023-04-14 12:01:26.342  D/DBINFO_AGFTN_STG3: ID is 3 NAME is AG_FTNULL_NULL_1
2023-04-14 12:01:26.343  D/DBINFO_AGFTN_STG3: ID is 4 NAME is AG_FTNULL_DEFAULT_1
2023-04-14 12:01:26.343  D/DBINFO_AGFTN_STG3: ID is 100 NAME is AG_FTNULL_100_1
2023-04-14 12:01:26.343  D/DBINFO_AGFTN_STG3: ID is 101 NAME is AG_FTNULL_NULL_2
2023-04-14 12:01:26.343  D/DBINFO_AGFTN_STG3: ID is 102 NAME is AG_FTNULL_DEFAULT_2
2023-04-14 12:01:26.343  D/DBINFO_AGFTN_STG3: ID is 103 NAME is AG_FTNULL_NULL_3
2023-04-14 12:01:26.343  D/DBINFO_AGFTN_STG3: ID is 104 NAME is AG_FTNULL_DEFAULT_3
2023-04-14 12:01:26.346  D/DBINFO_SSEQ_STG3: TABLE IS AutoGenTrueTable HIGHEST ID STORED FOR THE TABLE IS 110

结果解释(一点)

可以清楚地看到,id不是重复的(它们不能在所有情况下都是隐式唯一的,因为主键是隐式唯一的),而且sqlite_sequence只记录TT表使用过的最高id(autogenerate=true的那个)。
不太容易看到的是,autogenerate为false的地方,至少是隐含的/默认的,使用任何值超过一次,包括0,将不会生成id,也就是说Room将该值传递给insert。该演示具有IGNORE onConflictStrategy,因此这些尝试的重复被忽略,不会发生失败。
因此,对于FT和FTNULL,???_ZERO_nn的id为0,并且只有nn为1的单行。与TT_ZERO不同,TT_ZERO的所有3行都已插入,生成的ID不为0。

kyvafyod

kyvafyod2#

默认情况下,autoGenerate for primary key为false。这意味着默认情况下不会生成主键的随机id。我们需要指定autoGenerate = true以获取自动生成的id。
在本例中,每次在表中创建新对象时,都必须显式提供主键,因为它被指定为可空。
更多参考:主键

相关问题