首页 > 解决方案 > 如何确保 Room 中多个列的唯一约束,即使其中之一为空?

问题描述

我的应用程序中有一个 Room 数据库,其中一个表包含接收和发送的消息。在表格内部,消息仅通过电话号码进行区分,后端服务器为空(因为服务器没有电话号码)和已发送消息的用户电话号码。(在应用程序安装时输入,就像 Whatsapp 一样。)为了将表与后端同步,我引入了一个新列,其中包含服务器上消息的后端 ID。由于服务器将发送和接收的消息分开(由于后端表格中包含的信息不同),因此发送消息的id和接收消息的id可以相同,仅通过相应的电话号码来区分。(null 或 own)所以我在两列上创建了一个唯一约束:backend_id 和电话号码。

@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
}

但是尝试使用一些消息,我不得不意识到,如果电话号码为空,数据库很乐意接受相等的 backend_ids。为了确保这不是意外,我什至写了一个 UnitTest:

@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!!
            }
        }
    }
}

消息道:

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

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

标签: androidmysqldatabaseandroid-room

解决方案


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

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

例如,您可以使用:-

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


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

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

但是,Room 会发出错误消息,因为它不能确定backend_id列是被索引的列,因此会发出编译错误,例如:-

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

因此,您需要在 Room 创建表之外添加索引。你可以通过onCreateoronOpen回调来做到这一点。请注意,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 回调添加索引):-

    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 行(表中的第 3 行)将添加,由于 UNIQUE 冲突将忽略第 6 行。

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

1.消息表:-

在此处输入图像描述

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

在此处输入图像描述


推荐阅读