Android SQLite中基于复合主键查询已有行的实现方案
我有一张带有复合主键的表,使用Room的@Entity注解定义如下:
@Entity( tableName = "my_table", primaryKeys = ["key1", "key2", "key3"] ) data class MyTable( @ColumnInfo(name = "key1") val key1: Int, @ColumnInfo(name = "key2") val key2: Int, @ColumnInfo(name = "key3") var key3: String, ) { ... }
从远程数据源获取新记录后,希望基于复合主键判断哪些记录尚未存在于本地数据库。原本想使用如下SQL语句查询数据库中已有的行,但SQLite 3.9(对应Android API 24)不支持该语法:
SELECT * FROM table_name WHERE (key1, key2, key3) IN ( (1,2,'foo'), (3,4,'bar') );
请问基于远程列表查询已有行的正确方式是什么?
针对SQLite 3.9不支持复合值IN子句的问题,给你几个可行的实现思路:
方法1:用多个OR拼接主键条件
把每个要查的复合主键组合拆成AND条件,再用OR连起来就行。比如你示例里的两个组合,SQL可以写成:
SELECT * FROM my_table WHERE (key1 = 1 AND key2 = 2 AND key3 = 'foo') OR (key1 = 3 AND key2 = 4 AND key3 = 'bar');
在Room里可以动态拼接这个条件——比如先遍历远程获取的记录列表,把每个主键组合转换成对应的(key1=? AND key2=? AND key3=?)片段,再用OR串起来,最后通过@RawQuery或者带参数的DAO方法执行。要是列表长度固定,直接写死参数也没问题,但动态场景下拼接更灵活。
注意:如果要查的组合太多(比如上千条),这种方式会生成很长的SQL,可能影响性能,不过几十上百条的话完全没问题。
方法2:构造临时数据集做关联查询
用UNION ALL拼一个包含所有待查主键的临时表,再和原表做内连接,就能找出已存在的记录。SQL示例:
SELECT t.* FROM my_table t INNER JOIN ( SELECT 1 AS key1, 2 AS key2, 'foo' AS key3 UNION ALL SELECT 3 AS key1, 4 AS key2, 'bar' AS key3 ) temp ON t.key1 = temp.key1 AND t.key2 = temp.key2 AND t.key3 = temp.key3;
Room里可以通过代码动态生成UNION ALL的部分——比如遍历远程列表,把每个主键组合转换成SELECT ?, ?, ?的片段,用UNION ALL连接后拼进主SQL,再用@RawQuery执行。这种方式比OR拼接更整洁,数据多的时候性能也更稳定。
方法3:用Room的插入冲突策略间接判断
如果你的最终目的是把远程新记录插入本地(跳过已存在的),其实不用额外查,直接用Room的@Insert加onConflict = OnConflictStrategy.IGNORE就行。Room会自动忽略主键冲突的记录,你可以通过返回结果判断哪些是新增的。
比如DAO里写:
@Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertAll(myTables: List<MyTable>): List<Long>
返回的List<Long>里,每个元素是插入后的行ID,-1就代表这条记录因为已存在被跳过了。遍历这个列表,就能从远程列表里筛选出本地没有的记录。这种方式代码最简洁,不用写复杂的查询语句。
内容的提问来源于stack exchange,提问作者lostintranslation

