android - 如何确保 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>
}
解决方案
在 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 创建表之外添加索引。你可以通过onCreate
oronOpen
回调来做到这一点。请注意,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%';
):-
推荐阅读
- flutter - 在颤动中创建进度条指示器形状
- android - ExoPlayer 有时会从 url 返回 mp3 文件的负持续时间
- iexpress - iExpress 错误:“无法结束资源更新”
- mql4 - 我如何计算某一天的所有利润?
- java - BufferedImage 灰度图太暗
- c++ - 包含 glm 头文件时 QT Creator 中的编码错误
- asp.net - System.InvalidOperationException 包含授权元数据,但未找到支持授权的中间件
- firebase-authentication - 使用来自 CICP/Firebase 身份验证的访问令牌时,未配置返回 JWT 颁发者的 Cloud Endpoints 身份验证
- javascript - 为什么我的
线没有出现在我的 d3 折线图上? - vue.js - VueJs Vuetify 点击表格行事件未读取项目