如何确保对 Room 中多列的唯一约束,即使其中一列为空?

How to ensure unique constraint over multiple columns in Room, even if one of them is null?

我的应用程序中有一个 Room 数据库,其中一个 table 包含接收和发送的消息。在 table 内部,消息仅由 phone 数字区分,对于后端服务器为空(因为服务器没有 phone 数字)和 phone发送消息的用户数。 (在安装应用程序时输入,就像 Whatsapp 一样。) 为了将 table 与后端同步,我引入了一个新列,其中包含服务器上消息的后端 ID。由于服务器将发送消息和接收消息分开(由于tables后端包含的信息不同),因此发送消息的id和接收消息的id可以相等,仅通过对应的[=29区分=] 号。 (为空或拥有) 所以我在两列上创建了一个唯一约束:backend_id & phone number.

@Entity(indices = [Index(value = ["backend_id", "senderNumber"], unique = true)])
data class Message(
        var senderNumber: String?,
        var message: String?,
        var backend_id: String? = null,
        var time : Date? = Date(),
        var status : Status = Status.PENDING
) : ListItem(time), Serializable {
    @PrimaryKey(autoGenerate = true) var id : Long? = null
}

但是尝试使用一些消息,我必须意识到,如果 phone 数字为空,数据库很乐意接受相等的 backend_ids。为了确保这不是意外,我什至写了一个单元测试:

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    lateinit var db : MyDatabase
    lateinit var dao : MessageDao
    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(
                context, MyDatabase::class.java).build()
        dao = db.messageDao()
    }
    @After
    @Throws(IOException::class)
    fun closeDb() {
        db.close()
    }

    @Test(expected = Exception::class)
    fun check_unique_constraint_is_violated() {
        // Context of the app under test.
        val message = Message(senderNumber = null, backend_id = "1", time = Date(), message = "Hello")
        dao.insertAll(message)
        dao.insertAll(message)
        val allMessages = dao.getAll()
        assertTrue(allMessages.size==2)
        assertTrue(allMessages[0].backend_id==allMessages[1].backend_id)
    }
}

这个测试失败了,因为它没有抛出任何异常。调试显示,Room 数据库也没有静默捕获异常,因为两条消息(相同)都被成功插入,导致 2 条消息。

所以我的问题是:如何确保结果在两列中都是唯一的,即使其中一列为空?对我来说似乎有点奇怪,您可以传递唯一性,只需为其中一列插入 null 即可。它起作用了,当我只检查索引中的 backend_id 时,抛出异常,当发送和接收的消息具有相同的 id 时。 (但我显然不希望那样。)

如果 Database 和 Dao 与解决方案有任何关联: 数据库:

@Database(entities = [Message::class], version = 1)
@TypeConverters(Converters::class)
abstract class MyDatabase : RoomDatabase() {
    override fun init(configuration: DatabaseConfiguration) {
        super.init(configuration)
        //Create and execute some trigger, limiting the entries on the latest 50
    }
    abstract fun messageDao() : MessageDao
    companion object {
        private var db: MyDatabase? = null
        private fun create(context : Context) : MyDatabase {
            return Room.databaseBuilder(context, MyDatabase::class.java, "dbname").build()
        }
        fun getDB(context : Context) : MyDatabase {
            synchronized(this) {
                if(db==null) {
                    db = create(context)
                }
                return db!!
            }
        }
    }
}

MessageDao:

@Dao
interface MessageDao {
    @Query("SELECT * FROM Message")
    fun getAll() : List<Message>

    @Insert
    fun insertAll(vararg messages: Message) : List<Long>
}

在 SQLite(以及其他符合 SQL-92 的站点)中,null 被认为与任何其他 null 不同,因此是您的问题。

因此,您不应使用 null。您可以克服此问题,将默认值设置为指示未提供值的特定值。

例如你可以使用:-

@NotNull
var backend_id: String = "0000000000"
  • 0000000000 可以是适合您目的的任何值。
    • 也可以使用""。


备选 另一种方法是处理索引中的空值,例如:-

@Entity(indices = [Index(value = ["coalesce(backend_id,'00000000')", "senderNumber"], unique = true)])

HOWEVER,Room 会发出错误信息,因为它没有确定 backend_id 列是列被索引并因此发出编译错误,例如:-

error: coalesce(backend_id,'00000000') referenced in the index does not exists in the Entity.

因此您需要在 Room 创建 tables 之外添加索引。您可以通过 onCreateonOpen 回调来执行此操作。请注意 onCreate 仅在首次创建数据库时调用一次,而每次应用 运行.

时都会调用 onOpen

最安全(数据方面)但效率稍低的方法是使用 onOpen 回调。

这是一个创建索引的示例(将其应用于两列,考虑到 backend_id 和 senderNumber 列都可以为空)。

这是在构建数据库时完成的:- ....

                .addCallback(object :RoomDatabase.Callback() {
                    override fun onOpen(db: SupportSQLiteDatabase) {
                        super.onOpen(db)
                        db.execSQL(
                            "CREATE UNIQUE INDEX IF NOT EXISTS message_unique_sn_beid " +
                                    "ON message (coalesce(backend_id,''),coalesce(senderNumber,''));")
                    }
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                    }

                })
                .build()
                ....
  • 索引名称将是 message_unique_sn_beid

使用替代方案的结果

将消息实体基于您的(但列数较少)和 :-

的插入道
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(message: Message): Long

使用以下(并通过 onOpen 回调添加索引)当 运行ning :-

    dao.insert(Message(null,"1234567890","blah"))
    dao.insert(Message(null,"0123456789","blah","0123456789"))
    dao.insert(Message(null,"1234567890","blah"))
    dao.insert(Message(null,"1234567890","blah",null))
    dao.insert(Message(null,null,message = "blah",backend_id = "9876543210"))
    dao.insert(Message(null,null,message = "blah",backend_id = "9876543210"))
  • 将添加第 1 行和第 2 行,由于 UNIQUE 冲突将忽略第 3 行和第 4 行将添加第 5 行(table 中的第 3 行),由于 UNIQUE 冲突将忽略第 6 行。

使用 Android Studio 的数据库检查器:-

1.留言table:-

2. 查看以 mess 开头的项目的 sqlite_master(架构)(即 运行ning SQL SELECT * FROM sqlite_master WHERE name LIKE 'mess%';) :-